Добавил восстановление на случай застрявших темп файлов.  Оно работает!  И уведомление админа о критических ощибках, если файлы блокчейна поврежденны
This commit is contained in:
AidarKC 2025-12-25 17:08:08 +03:00
parent 6c2449f623
commit 25aa57dc5e
10 changed files with 139 additions and 85 deletions

1
.idea/gradle.xml generated
View File

@ -14,6 +14,7 @@
<option value="$PROJECT_DIR$/shine-server-crypto" /> <option value="$PROJECT_DIR$/shine-server-crypto" />
<option value="$PROJECT_DIR$/shine-server-db" /> <option value="$PROJECT_DIR$/shine-server-db" />
<option value="$PROJECT_DIR$/shine-server-geo" /> <option value="$PROJECT_DIR$/shine-server-geo" />
<option value="$PROJECT_DIR$/shine-server-log" />
<option value="$PROJECT_DIR$/shine-server-net-protocol" /> <option value="$PROJECT_DIR$/shine-server-net-protocol" />
<option value="$PROJECT_DIR$/shine-server-net-server" /> <option value="$PROJECT_DIR$/shine-server-net-server" />
</set> </set>

View File

@ -39,6 +39,7 @@ dependencies {
implementation project(':shine-server-config') // модуль настроек из application.properties implementation project(':shine-server-config') // модуль настроек из application.properties
implementation project(":shine-server-log") // модуль логирования и уведомления админов
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД

View File

@ -1,5 +1,6 @@
rootProject.name = 'shine-server-server' rootProject.name = 'shine-server-server'
include 'shine-server-log'
include 'shine-server-config' include 'shine-server-config'
include 'shine-server-geo' include 'shine-server-geo'
include 'shine-server-crypto' include 'shine-server-crypto'

View File

@ -25,6 +25,8 @@ dependencies {
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
implementation project(":shine-server-log") // модуль логирования и уведомления админов
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
} }

View File

@ -1,59 +0,0 @@
//package utils.files;
//
//import java.nio.charset.StandardCharsets;
//import java.util.Arrays;
//
///**
// * ===============================================================
// * FileStoreUtilSelfTest запускаемый тест утилиты FileStoreUtil.
// * ---------------------------------------------------------------
// * Сценарий:
// * 1) Создаём «блокчейн-файл» для id=20251021 с начальными данными.
// * 2) Дозаписываем ещё порцию данных.
// * 3) Читаем целиком и печатаем длину + превью.
// *.
// * Ожидаемый итог:
// * В папке "data" появится файл "20251021.bch"
// * В консоли будет длина содержимого и небольшой превью-дамп.
// * ===============================================================
// */
//public class FileStoreUtilSelfTest {
//
// public static void main(String[] args) {
// System.out.println("=== FileStoreUtil self-test ===");
//
// FileStoreUtil fs = FileStoreUtil.getInstance();
//
// long blockchainId = 20251021L;
//
// byte[] part1 = "Hello ".getBytes(StandardCharsets.UTF_8);
// byte[] part2 = "Blockchain!".getBytes(StandardCharsets.UTF_8);
//
// // 1) создаём новый файл для «блокчейна»
// fs.newBlockchain(blockchainId, part1);
//
// // 2) дозаписываем данные
// fs.addDataToBlockchain(blockchainId, part2);
//
// // 3) читаем всё содержимое и показываем превью
// byte[] all = fs.readAllDataFromBlockchain(blockchainId);
// System.out.println("Total bytes read: " + all.length);
// System.out.println("Preview (UTF-8): " + new String(all, StandardCharsets.UTF_8));
//
// // небольшой hex-дамп первых 32 байт (для наглядности)
// System.out.println("Preview (HEX 32B): " + toHexPreview(all, 32));
//
// System.out.println("✅ Self-test passed (файл: data/" + blockchainId + FileStoreUtil.BLOCKCHAIN_FILE_EXTENSION + ")");
// }
//
// private static String toHexPreview(byte[] data, int max) {
// int n = Math.min(data.length, max);
// StringBuilder sb = new StringBuilder(n * 2);
// for (int i = 0; i < n; i++) {
// sb.append(String.format("%02X", data[i]));
// if (i + 1 < n) sb.append(' ');
// }
// if (data.length > n) sb.append(" ...");
// return sb.toString();
// }
//}

View File

@ -0,0 +1,27 @@
plugins {
id 'java'
}
group = 'shine'
version = '1.0.0'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@ -1,4 +1,4 @@
package server.ws; package shine.log;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory;
* - пишет МАКСИМАЛЬНО ЗАМЕТНЫЙ лог * - пишет МАКСИМАЛЬНО ЗАМЕТНЫЙ лог
* *
* TODO: * TODO:
* - отправка уведомления администратору: * - реальная отправка уведомления администратору:
* * Telegram bot / email / SMS / webhook / Sentry / PagerDuty * * Telegram bot / email / SMS / webhook / Sentry / PagerDuty
* * желательно с hostname, временем, именем блокчейна, размерами и stacktrace * * желательно с hostname, временем, именем блокчейна, размерами и stacktrace
* =============================================================== * ===============================================================
@ -23,17 +23,21 @@ public final class BlockchainAdminNotifier {
private BlockchainAdminNotifier() {} private BlockchainAdminNotifier() {}
public static void critical(String message) {
critical(message, null);
}
public static void critical(String message, Throwable t) { public static void critical(String message, Throwable t) {
String bannerTop = String bannerTop =
"\n" + "\n" +
"=================================================================\n" + "=================================================================\n" +
"==================== !!! CRITICAL ALERT !!! ===================\n" + "==================== !!! КРИТИЧЕСКАЯ ОШИБКА !!! ===============\n" +
"================================================================="; "=================================================================";
String bannerBottom = String bannerBottom =
"=================================================================\n" + "=================================================================\n" +
"==================== !!! ACTION REQUIRED !!! ===================\n" + "==================== !!! НУЖНО ВМЕШАТЕЛЬСТВО !!! ===============\n" +
"=================================================================\n"; "=================================================================\n";
if (t == null) { if (t == null) {
@ -51,6 +55,6 @@ public final class BlockchainAdminNotifier {
); );
} }
// TODO: Реальная отправка уведомления администратору (telegram/email/webhook/sentry) // TODO: Реальная отправка уведомления администратору (Telegram/email/webhook/Sentry)
} }
} }

View File

@ -24,7 +24,9 @@ dependencies {
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
implementation project(':shine-server-config') // модуль с настройками implementation project(':shine-server-config') // модуль с настройками
implementation project(":shine-server-log") // модуль логирования и уведомления админов
implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией
implementation project(':shine-server-blockchain') // модуль для работы с блокчейном implementation project(':shine-server-blockchain') // модуль для работы с блокчейном
implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД

View File

@ -9,6 +9,7 @@ import shine.db.dao.BlocksDAO;
import shine.db.entities.BlockEntry; import shine.db.entities.BlockEntry;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import shine.log.BlockchainAdminNotifier;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -26,7 +27,12 @@ import java.sql.SQLException;
* Важно: * Важно:
* - Шаг (2) строго атомарный (SQL tx). * - Шаг (2) строго атомарный (SQL tx).
* - Шаг (3) атомарный на уровне ФС, если поддерживается ATOMIC_MOVE. * - Шаг (3) атомарный на уровне ФС, если поддерживается ATOMIC_MOVE.
* - Если сервер упадёт между (2) и (3), останется tmp твой recovery при старте починит. *
* ДОПОЛНЕНИЕ (КРИТИЧНО):
* - Перед тем как дописывать блок, проверяем:
* реальный размер <name>.bch == st.fileSizeBytes.
* Если не совпадает считаем это критической внешней порчей файлов,
* шлём уведомление админу и НЕ продолжаем запись.
*/ */
public final class BlockchainWriter { public final class BlockchainWriter {
@ -46,6 +52,7 @@ public final class BlockchainWriter {
/** /**
* Главный метод: * Главный метод:
* - (0) проверяет соответствие размера файла и state (если это не genesis)
* - создаёт tmp-файл (старое+новое), * - создаёт tmp-файл (старое+новое),
* - атомарно коммитит БД (block+state), * - атомарно коммитит БД (block+state),
* - атомарно заменяет основной файл. * - атомарно заменяет основной файл.
@ -59,6 +66,15 @@ public final class BlockchainWriter {
String newHashHex String newHashHex
) throws SQLException { ) throws SQLException {
// =====================================================================
// ШАГ 0. КРИТИЧЕСКАЯ ПРОВЕРКА КОНСИСТЕНТНОСТИ:
// - если state есть и ожидает ненулевой размер,
// то основной файл должен существовать и иметь точно этот размер.
// - если не так это почти наверняка внешнее вмешательство/порча,
// и продолжать запись НЕЛЬЗЯ.
// =====================================================================
verifyMainFileSizeMatchesStateOrAlert(login, blockchainName, block, stOrNull);
// ===================================================================== // =====================================================================
// ШАГ 1. Готовим bytes нового блока (включая signature+hash) // ШАГ 1. Готовим bytes нового блока (включая signature+hash)
// ===================================================================== // =====================================================================
@ -90,19 +106,22 @@ public final class BlockchainWriter {
try { try {
oldBytes = fs.readBlockchain(blockchainName); oldBytes = fs.readBlockchain(blockchainName);
} catch (Exception e) { } catch (Exception e) {
// Добавили подробный лог: это очень важная точка
log.error("Ошибка чтения старого файла блокчейна перед записью tmp (login={}, blockchainName={}, oldFileSize={}, blockNumber={})", log.error("Ошибка чтения старого файла блокчейна перед записью tmp (login={}, blockchainName={}, oldFileSize={}, blockNumber={})",
login, blockchainName, oldFileSize, block.recordNumber, e); login, blockchainName, oldFileSize, block.recordNumber, e);
// Здесь лучше падать: state говорит, что файл есть, а прочитать нельзя.
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e); throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
} }
// (на будущее) можно проверять согласованность: oldBytes.length == oldFileSize // (в идеале это всегда должно совпадать после verifyMainFileSizeMatchesStateOrAlert)
// но ты всё равно будешь делать recovery при старте оставим как подсказку.
if (oldBytes.length != (int) oldFileSize) { if (oldBytes.length != (int) oldFileSize) {
log.warn("Несовпадение размера файла блокчейна: state говорит oldFileSize={}, а реально прочитали oldBytes.length={} (login={}, blockchainName={}, blockNumber={})", String msg =
oldFileSize, oldBytes.length, login, blockchainName, block.recordNumber); "Несовпадение размера файла блокчейна при чтении: " +
"state ожидал oldFileSize=" + oldFileSize +
", а реально прочитали oldBytes.length=" + oldBytes.length +
" (login=" + login +
", blockchainName=" + blockchainName +
", blockNumber=" + block.recordNumber + ").";
BlockchainAdminNotifier.critical(msg, null);
throw new SQLException(msg);
} }
tmpBytes = concat(oldBytes, newBlockFullBytes); tmpBytes = concat(oldBytes, newBlockFullBytes);
@ -113,7 +132,6 @@ public final class BlockchainWriter {
try { try {
fs.writeBlockchainTmp(blockchainName, tmpBytes); fs.writeBlockchainTmp(blockchainName, tmpBytes);
} catch (Exception e) { } catch (Exception e) {
// Добавили подробный лог: это тоже критично
log.error("Ошибка записи tmp файла блокчейна (login={}, blockchainName={}, tmpBytesLen={}, oldFileSize={}, newFileSize={}, blockNumber={})", log.error("Ошибка записи tmp файла блокчейна (login={}, blockchainName={}, tmpBytesLen={}, oldFileSize={}, newFileSize={}, blockNumber={})",
login, blockchainName, tmpBytes.length, oldFileSize, newFileSize, block.recordNumber, e); login, blockchainName, tmpBytes.length, oldFileSize, newFileSize, block.recordNumber, e);
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e); throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
@ -145,7 +163,6 @@ public final class BlockchainWriter {
} catch (Exception e) { } catch (Exception e) {
try { c.rollback(); } catch (SQLException ignore) {} try { c.rollback(); } catch (SQLException ignore) {}
// ВАЖНО: логируем причину отката + контекст
log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevHash={}, newHash={}, oldFileSize={}, newFileSize={})", log.error("Ошибка транзакции БД при добавлении блока (rollback выполнен) (login={}, blockchainName={}, blockNumber={}, prevHash={}, newHash={}, oldFileSize={}, newFileSize={})",
login, blockchainName, block.recordNumber, prevGlobalHashHex, newHashHex, oldFileSize, newFileSize, e); login, blockchainName, block.recordNumber, prevGlobalHashHex, newHashHex, oldFileSize, newFileSize, e);
@ -159,23 +176,14 @@ public final class BlockchainWriter {
// ================================================================= // =================================================================
// ШАГ 5. После успешного коммита БД атомарно заменяем файл: // ШАГ 5. После успешного коммита БД атомарно заменяем файл:
// <name>.tmp_bch -> <name>.bch // <name>.tmp_bch -> <name>.bch
//
// Если тут упадём:
// - БД уже обновлена
// - tmp остаётся
// - recovery при старте восстановит консистентность
// ================================================================= // =================================================================
if (committed) { if (committed) {
try { try {
fs.atomicReplaceBlockchainFile(blockchainName); fs.atomicReplaceBlockchainFile(blockchainName);
} catch (Exception moveError) { } catch (Exception moveError) {
// Очень важная ситуация: БД уже committed, а файл не заменился
log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={}, newHash={}, tmpBytesLen={})", log.error("БД закоммичена, но атомарная замена файла блокчейна не удалась. tmp оставлен для recovery. (login={}, blockchainName={}, blockNumber={}, newHash={}, tmpBytesLen={})",
login, blockchainName, block.recordNumber, newHashHex, tmpBytes.length, moveError); login, blockchainName, block.recordNumber, newHashHex, tmpBytes.length, moveError);
// Здесь ВАЖНО: мы уже не можем откатить БД.
// Оставляем tmp и даём наверх ошибку клиент увидит internal_error,
// а ты при старте починишь файловую часть.
throw new SQLException( throw new SQLException(
"DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName, "DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName,
moveError moveError
@ -185,6 +193,73 @@ public final class BlockchainWriter {
} }
} }
/**
* Проверка: реальный размер <name>.bch должен совпадать с st.fileSizeBytes.
* Если нет это критическая внешняя порча/вмешательство, уведомляем админа и падаем.
*/
private void verifyMainFileSizeMatchesStateOrAlert(
String login,
String blockchainName,
BchBlockEntry block,
BlockchainStateEntry stOrNull
) throws SQLException {
if (stOrNull == null) {
// genesis state ещё нет, проверять нечего
return;
}
long expected = stOrNull.getFileSizeBytes();
if (expected <= 0) {
// state есть, но ожидаемый размер 0 это либо пустая цепочка, либо старый формат.
// Здесь не трогаем (но можно усилить правила позже).
return;
}
String mainFileName = fs.buildBlockchainFileName(blockchainName);
// Если файла нет это уже очень подозрительно: state говорит файл есть и размер > 0
if (!fs.exists(mainFileName)) {
String msg =
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: state ожидает основной файл, но его нет. " +
"login=" + login +
", blockchainName=" + blockchainName +
", expectedSizeFromState=" + expected +
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
BlockchainAdminNotifier.critical(msg, null);
throw new SQLException(msg);
}
long real;
try {
real = fs.size(mainFileName);
} catch (Exception e) {
String msg =
"КРИТИЧЕСКАЯ ОШИБКА: не удалось получить размер основного файла блокчейна. " +
"login=" + login +
", blockchainName=" + blockchainName +
", expectedSizeFromState=" + expected +
", blockNumber=" + (block != null ? block.recordNumber : -1) + ".";
BlockchainAdminNotifier.critical(msg, e);
throw new SQLException(msg, e);
}
if (real != expected) {
String msg =
"КРИТИЧЕСКАЯ ОШИБКА КОНСИСТЕНТНОСТИ: размер файла блокчейна НЕ СОВПАДАЕТ с state. " +
"login=" + login +
", blockchainName=" + blockchainName +
", expectedSizeFromState=" + expected +
", realMainFileSize=" + real +
", blockNumber=" + (block != null ? block.recordNumber : -1) + ". " +
"Похоже на внешнее вмешательство/порчу файла. Запись нового блока остановлена.";
BlockchainAdminNotifier.critical(msg, null);
throw new SQLException(msg);
}
}
/** /**
* Обновление состояния blockchain_state (создаём если отсутствует). * Обновление состояния blockchain_state (создаём если отсутствует).
* Пока линии не используются: lineIndex=0 и lineHash = globalHash. * Пока линии не используются: lineIndex=0 и lineHash = globalHash.
@ -274,7 +349,6 @@ public final class BlockchainWriter {
} }
private static long safeAdd(long x, long y) { private static long safeAdd(long x, long y) {
// защита от переполнения long (маловероятно, но пусть будет)
long r = x + y; long r = x + y;
if (((x ^ r) & (y ^ r)) < 0) { if (((x ^ r) & (y ^ r)) < 0) {
throw new IllegalArgumentException("fileSizeBytes overflow: " + x + " + " + y); throw new IllegalArgumentException("fileSizeBytes overflow: " + x + " + " + y);

View File

@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
import shine.db.dao.BlockchainStateDAO; import shine.db.dao.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry; import shine.db.entities.BlockchainStateEntry;
import utils.files.FileStoreUtil; import utils.files.FileStoreUtil;
import shine.log.BlockchainAdminNotifier;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;