25 12 25
Добавил восстановление на случай застрявших темп файлов. Оно работает!
This commit is contained in:
parent
d8057807a3
commit
6c2449f623
56
src/main/java/server/ws/BlockchainAdminNotifier.java
Normal file
56
src/main/java/server/ws/BlockchainAdminNotifier.java
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package server.ws;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================================================
|
||||||
|
* BlockchainAdminNotifier — уведомления администратору о критических
|
||||||
|
* ошибках консистентности блокчейн-файлов.
|
||||||
|
*
|
||||||
|
* Сейчас:
|
||||||
|
* - пишет МАКСИМАЛЬНО ЗАМЕТНЫЙ лог
|
||||||
|
*
|
||||||
|
* TODO:
|
||||||
|
* - отправка уведомления администратору:
|
||||||
|
* * Telegram bot / email / SMS / webhook / Sentry / PagerDuty
|
||||||
|
* * желательно с hostname, временем, именем блокчейна, размерами и stacktrace
|
||||||
|
* ===============================================================
|
||||||
|
*/
|
||||||
|
public final class BlockchainAdminNotifier {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BlockchainAdminNotifier.class);
|
||||||
|
|
||||||
|
private BlockchainAdminNotifier() {}
|
||||||
|
|
||||||
|
public static void critical(String message, Throwable t) {
|
||||||
|
|
||||||
|
String bannerTop =
|
||||||
|
"\n" +
|
||||||
|
"=================================================================\n" +
|
||||||
|
"==================== !!! CRITICAL ALERT !!! ===================\n" +
|
||||||
|
"=================================================================";
|
||||||
|
|
||||||
|
String bannerBottom =
|
||||||
|
"=================================================================\n" +
|
||||||
|
"==================== !!! ACTION REQUIRED !!! ===================\n" +
|
||||||
|
"=================================================================\n";
|
||||||
|
|
||||||
|
if (t == null) {
|
||||||
|
log.error("{}\n{}\n{}",
|
||||||
|
bannerTop,
|
||||||
|
message,
|
||||||
|
bannerBottom
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.error("{}\n{}\n{}",
|
||||||
|
bannerTop,
|
||||||
|
message,
|
||||||
|
bannerBottom,
|
||||||
|
t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Реальная отправка уведомления администратору (telegram/email/webhook/sentry)
|
||||||
|
}
|
||||||
|
}
|
||||||
251
src/main/java/server/ws/BlockchainTmpRecoveryOnStartup.java
Normal file
251
src/main/java/server/ws/BlockchainTmpRecoveryOnStartup.java
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
package server.ws;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
|
import utils.files.FileStoreUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===============================================================
|
||||||
|
* BlockchainTmpRecoveryOnStartup — восстановление консистентности
|
||||||
|
* blockchain файлов при старте сервера.
|
||||||
|
*
|
||||||
|
* Сценарий проблемы:
|
||||||
|
* - при добавлении блока сначала пишется <name>.tmp_bch
|
||||||
|
* - потом коммитится БД (state.fileSizeBytes)
|
||||||
|
* - потом tmp переименовывается поверх <name>.bch (атомарно, если возможно)
|
||||||
|
*
|
||||||
|
* Если сервер упал в середине, может остаться tmp:
|
||||||
|
* - tmp есть, а основной .bch остался старым
|
||||||
|
* - tmp есть, а основной .bch уже удалили/заменить не успели
|
||||||
|
* - tmp есть, а БД успела/не успела обновиться
|
||||||
|
*
|
||||||
|
* Этот класс при старте:
|
||||||
|
* - ищет все *.tmp_bch в data/
|
||||||
|
* - сравнивает размеры:
|
||||||
|
* - tmp
|
||||||
|
* - main (если есть)
|
||||||
|
* - state.fileSizeBytes (если есть)
|
||||||
|
*
|
||||||
|
* Правила:
|
||||||
|
*
|
||||||
|
* A) state есть:
|
||||||
|
* - если stateSize == mainSize => tmp удаляем
|
||||||
|
* - если stateSize == tmpSize => tmp ставим на место main (atomicReplaceBlockchainFile)
|
||||||
|
* - иначе => КРИТИЧЕСКАЯ ОШИБКА: сервер останавливаем + уведомление администратору
|
||||||
|
*
|
||||||
|
* B) state НЕТ:
|
||||||
|
* - если main НЕТ и tmp ЕСТЬ => tmp удаляем (мусор после падения/неуспешной транзакции)
|
||||||
|
* - если main ЕСТЬ и tmp ЕСТЬ => КРИТИЧЕСКАЯ ОШИБКА: уведомление администратору + стоп сервера
|
||||||
|
*
|
||||||
|
* Логирование:
|
||||||
|
* - обо всех восстановленных/удалённых tmp пишем в лог
|
||||||
|
* - если tmp-файлов нет — тоже пишем в лог
|
||||||
|
* ===============================================================
|
||||||
|
*/
|
||||||
|
public final class BlockchainTmpRecoveryOnStartup {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BlockchainTmpRecoveryOnStartup.class);
|
||||||
|
|
||||||
|
private BlockchainTmpRecoveryOnStartup() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск восстановления.
|
||||||
|
* Если обнаружена ситуация, когда размеры не совпали и сервер сам не может чинить — бросаем исключение.
|
||||||
|
*/
|
||||||
|
public static void runRecoveryOrThrow() {
|
||||||
|
FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||||
|
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||||
|
|
||||||
|
Path dataDir = Paths.get(FileStoreUtil.DATA_DIR_NAME);
|
||||||
|
ensureDirExists(dataDir);
|
||||||
|
|
||||||
|
List<Path> tmpFiles = listTmpFiles(dataDir);
|
||||||
|
|
||||||
|
if (tmpFiles.isEmpty()) {
|
||||||
|
log.info("🟢 BlockchainTmpRecovery: временных *.tmp_bch файлов не найдено — восстановление не требуется.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("🟡 BlockchainTmpRecovery: найдено временных файлов: {}", tmpFiles.size());
|
||||||
|
|
||||||
|
for (Path tmpPath : tmpFiles) {
|
||||||
|
String fileName = tmpPath.getFileName().toString();
|
||||||
|
String blockchainName = extractBlockchainNameFromTmp(fileName);
|
||||||
|
|
||||||
|
if (blockchainName == null || blockchainName.isBlank()) {
|
||||||
|
// странное имя — не трогаем автоматически, но это уже повод дернуть админа
|
||||||
|
BlockchainAdminNotifier.critical(
|
||||||
|
"НАЙДЕН TMP-ФАЙЛ С НЕОЖИДАННЫМ ИМЕНЕМ: " + fileName + " (не могу определить blockchainName).",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
throw new IllegalStateException("Bad tmp file name: " + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path mainPath = dataDir.resolve(fs.buildBlockchainFileName(blockchainName));
|
||||||
|
|
||||||
|
long tmpSize = safeSize(tmpPath);
|
||||||
|
boolean mainExists = Files.exists(mainPath);
|
||||||
|
long mainSize = mainExists ? safeSize(mainPath) : -1L;
|
||||||
|
|
||||||
|
BlockchainStateEntry st = null;
|
||||||
|
try {
|
||||||
|
st = stateDAO.getByBlockchainName(blockchainName);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
BlockchainAdminNotifier.critical(
|
||||||
|
"ОШИБКА БД ПРИ ВОССТАНОВЛЕНИИ TMP: blockchainName=" + blockchainName + " (сервер остановлен).",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw new IllegalStateException("DB error during tmp recovery for " + blockchainName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CASE B) state НЕТ
|
||||||
|
// ============================================================
|
||||||
|
if (st == null) {
|
||||||
|
|
||||||
|
if (!mainExists) {
|
||||||
|
// НЕТ state, НЕТ main, есть tmp => удаляем tmp
|
||||||
|
log.warn("🟠 BlockchainTmpRecovery: state отсутствует и main отсутствует, но tmp найден => удаляем tmp. blockchainName={}, tmpSize={}",
|
||||||
|
blockchainName, tmpSize);
|
||||||
|
safeDelete(tmpPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// НЕТ state, но main есть и tmp есть => это уже подозрительно
|
||||||
|
BlockchainAdminNotifier.critical(
|
||||||
|
"НЕСОГЛАСОВАННОСТЬ: ЕСТЬ main И tmp, НО НЕТ state В БД. " +
|
||||||
|
"blockchainName=" + blockchainName +
|
||||||
|
", mainSize=" + mainSize +
|
||||||
|
", tmpSize=" + tmpSize +
|
||||||
|
". СЕРВЕР ОСТАНОВЛЕН. " +
|
||||||
|
"ПОДОЗРЕНИЕ: файлы могли быть изменены вне сервера.",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
throw new IllegalStateException("State missing but both main and tmp exist for " + blockchainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CASE A) state ЕСТЬ
|
||||||
|
// ============================================================
|
||||||
|
long stateSize = st.getFileSizeBytes();
|
||||||
|
|
||||||
|
// 1) stateSize == mainSize => tmp мусор
|
||||||
|
if (mainExists && mainSize == stateSize) {
|
||||||
|
log.info("🟢 BlockchainTmpRecovery: stateSize совпадает с main => tmp удаляем. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
|
||||||
|
blockchainName, stateSize, mainSize, tmpSize);
|
||||||
|
safeDelete(tmpPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) stateSize == tmpSize => tmp это актуальная версия, ставим на место main
|
||||||
|
if (tmpSize == stateSize) {
|
||||||
|
log.warn("🟡 BlockchainTmpRecovery: stateSize совпадает с tmp => восстанавливаем main из tmp. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
|
||||||
|
blockchainName, stateSize, mainSize, tmpSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// метод уже есть и делает move tmp->main с попыткой ATOMIC_MOVE
|
||||||
|
fs.atomicReplaceBlockchainFile(blockchainName);
|
||||||
|
|
||||||
|
// после move tmp должен исчезнуть сам (перемещён)
|
||||||
|
log.info("✅ BlockchainTmpRecovery: восстановление выполнено. blockchainName={}, newMainSize={}",
|
||||||
|
blockchainName, safeSize(mainPath));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
BlockchainAdminNotifier.critical(
|
||||||
|
"НЕ УДАЛОСЬ ВОССТАНОВИТЬ main ИЗ tmp (move failed). " +
|
||||||
|
"blockchainName=" + blockchainName +
|
||||||
|
", stateSize=" + stateSize +
|
||||||
|
", mainSize=" + mainSize +
|
||||||
|
", tmpSize=" + tmpSize +
|
||||||
|
". СЕРВЕР ОСТАНОВЛЕН.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw new IllegalStateException("Cannot replace main from tmp for " + blockchainName, e);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) НИЧЕГО НЕ СОВПАЛО => критическая ситуация
|
||||||
|
BlockchainAdminNotifier.critical(
|
||||||
|
"ФАТАЛЬНАЯ НЕСОГЛАСОВАННОСТЬ BLOCKCHAIN ФАЙЛОВ. " +
|
||||||
|
"blockchainName=" + blockchainName +
|
||||||
|
", stateSize=" + stateSize +
|
||||||
|
", mainExists=" + mainExists +
|
||||||
|
", mainSize=" + mainSize +
|
||||||
|
", tmpSize=" + tmpSize +
|
||||||
|
". СЕРВЕР ОСТАНОВЛЕН. " +
|
||||||
|
"ТУТ НУЖНО УВЕДОМЛЕНИЕ АДМИНИСТРАТОРУ: возможно файлы изменены вручную/другой программой.",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
throw new IllegalStateException("Blockchain files mismatch for " + blockchainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("✅ BlockchainTmpRecovery: обработка tmp-файлов завершена.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* =============================== Helpers ============================== */
|
||||||
|
/* ===================================================================== */
|
||||||
|
|
||||||
|
private static void ensureDirExists(Path dir) {
|
||||||
|
try {
|
||||||
|
if (!Files.exists(dir)) {
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Cannot create data dir: " + dir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Path> listTmpFiles(Path dataDir) {
|
||||||
|
List<Path> out = new ArrayList<>();
|
||||||
|
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dataDir, "*" + FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) {
|
||||||
|
for (Path p : ds) {
|
||||||
|
if (Files.isRegularFile(p)) out.add(p);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Cannot list tmp files in: " + dataDir, e);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Из "anya0001.tmp_bch" -> "anya0001"
|
||||||
|
*/
|
||||||
|
private static String extractBlockchainNameFromTmp(String tmpFileName) {
|
||||||
|
if (tmpFileName == null) return null;
|
||||||
|
if (!tmpFileName.endsWith(FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) return null;
|
||||||
|
|
||||||
|
String base = tmpFileName.substring(0, tmpFileName.length() - FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION.length());
|
||||||
|
|
||||||
|
// базовая защита: не допускаем слэши/.. даже если кто-то подложил файл
|
||||||
|
if (base.isBlank()) return null;
|
||||||
|
if (base.contains("/") || base.contains("\\") || base.contains("..")) return null;
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long safeSize(Path p) {
|
||||||
|
try {
|
||||||
|
return Files.size(p);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Cannot read file size: " + p, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void safeDelete(Path p) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(p);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Cannot delete file: " + p, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,12 +10,34 @@ import utils.config.AppConfig;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WsServer — поднимает Jetty WS на /ws (порт 8080).
|
* WsServer — поднимает Jetty WS на /ws.
|
||||||
|
*
|
||||||
|
* ВАЖНО:
|
||||||
|
* - перед стартом сервера выполняем recovery tmp-блокчейнов.
|
||||||
|
* - если обнаружена несогласованность, которую сервер сам чинить не может —
|
||||||
|
* recovery бросает исключение и сервер не стартует.
|
||||||
*/
|
*/
|
||||||
public final class WsServer {
|
public final class WsServer {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WsServer.class);
|
private static final Logger log = LoggerFactory.getLogger(WsServer.class);
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 0) Восстановление консистентности blockchain файлов
|
||||||
|
// ============================================================
|
||||||
|
try {
|
||||||
|
BlockchainTmpRecoveryOnStartup.runRecoveryOrThrow();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Уже должно быть “большое” уведомление через BlockchainAdminNotifier,
|
||||||
|
// но на всякий случай логируем ещё раз.
|
||||||
|
log.error("❌ Сервер НЕ будет запущен: критическая ошибка восстановления blockchain tmp-файлов.", e);
|
||||||
|
throw e; // останавливаем запуск
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 1) Настройки порта
|
||||||
|
// ============================================================
|
||||||
AppConfig config = AppConfig.getInstance();
|
AppConfig config = AppConfig.getInstance();
|
||||||
int port = 7070;
|
int port = 7070;
|
||||||
try {
|
try {
|
||||||
@ -27,7 +49,9 @@ public final class WsServer {
|
|||||||
log.info("Не удалось прочитать параметр server.port, используем порт по умолчанию {}", port);
|
log.info("Не удалось прочитать параметр server.port, используем порт по умолчанию {}", port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 2) Запуск Jetty WS
|
||||||
|
// ============================================================
|
||||||
Server server = new Server(port);
|
Server server = new Server(port);
|
||||||
|
|
||||||
ServletContextHandler context = new ServletContextHandler();
|
ServletContextHandler context = new ServletContextHandler();
|
||||||
@ -47,4 +71,4 @@ public final class WsServer {
|
|||||||
log.info("✅ WS сервер запущен на ws://localhost:{}/ws", port);
|
log.info("✅ WS сервер запущен на ws://localhost:{}/ws", port);
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
Работу с линиями
|
||||||
|
|
||||||
|
Восстановление при подвисании сервера
|
||||||
Loading…
Reference in New Issue
Block a user