diff --git a/backend/package.json b/backend/package.json index ef6f7cb3..f5cdd2a3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,7 @@ "test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts", "test:containers": "tsx src/services/containers-state.test.ts && tsx src/routes/containers-state.test.ts", "test:certificates": "tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts", - "test:system-database": "tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts" + "test:system-database": "tsx src/services/traffic-flow-worker-shutdown.test.ts && tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", diff --git a/backend/src/services/serial-operation.ts b/backend/src/services/serial-operation.ts new file mode 100644 index 00000000..230371a8 --- /dev/null +++ b/backend/src/services/serial-operation.ts @@ -0,0 +1,9 @@ +/** Each caller observes its own result; a rejection does not break the queue. */ +export function createSerialOperation() { + let tail: Promise = Promise.resolve() + return (run: () => Promise): Promise => { + const result = tail.then(run, run) + tail = result.catch(() => {}) + return result + } +} diff --git a/backend/src/services/system-database-backup.ts b/backend/src/services/system-database-backup.ts index 42b853bf..70c03a72 100644 --- a/backend/src/services/system-database-backup.ts +++ b/backend/src/services/system-database-backup.ts @@ -10,8 +10,8 @@ import { withOperationLock } from "../db/operation-lock.js" import { executeDatabaseOperation, type DatabaseOperationWarnings } from "./system-database-operation.js" import { restartSchedulerAfterDatabaseOperation, stopSchedulerForDatabaseOperation } from "./scheduler.js" import { - startTrafficFlowListener, - stopTrafficFlowListener, + restartTrafficFlowListenerAfterDatabaseOperation, + stopTrafficFlowListenerForDatabaseOperation, } from "./traffic-flow-ingest.js" const BACKUP_LOCK_KEY = 8_723_102 @@ -48,9 +48,9 @@ function spawnCollected(command: string, args: string[], opts?: { stdin?: Buffer async function withDatabaseOperation(fn: () => Promise): Promise { return withOperationLock(() => pool.connect(), BACKUP_LOCK_KEY, () => executeDatabaseOperation(fn, { - stopListener: stopTrafficFlowListener, + stopListener: stopTrafficFlowListenerForDatabaseOperation, stopScheduler: stopSchedulerForDatabaseOperation, - startListener: startTrafficFlowListener, + startListener: restartTrafficFlowListenerAfterDatabaseOperation, startScheduler: restartSchedulerAfterDatabaseOperation, }), "Операция с базой приложения уже выполняется") } diff --git a/backend/src/services/traffic-flow-collector-ipc.ts b/backend/src/services/traffic-flow-collector-ipc.ts index 35cd3936..d605c577 100644 --- a/backend/src/services/traffic-flow-collector-ipc.ts +++ b/backend/src/services/traffic-flow-collector-ipc.ts @@ -32,9 +32,11 @@ export interface CollectorHeartbeat { export type MainToWorker = | { type: "start"; payload: CollectorStartPayload } | { type: "stop" } + | { type: "shutdown" } | { type: "updateExporterMap"; payload: ExporterMapPayload } | { type: "updateSettings"; payload: { topN: number; retentionHours: number } } export type WorkerToMain = | { type: "heartbeat"; payload: CollectorHeartbeat } | { type: "error"; payload: { message: string } } + | { type: "shutdown-complete" } diff --git a/backend/src/services/traffic-flow-collector-worker.ts b/backend/src/services/traffic-flow-collector-worker.ts index 9af2bbac..4b7cdab6 100644 --- a/backend/src/services/traffic-flow-collector-worker.ts +++ b/backend/src/services/traffic-flow-collector-worker.ts @@ -1,5 +1,6 @@ import { createSocket, type Socket } from "node:dgram" import { parentPort } from "node:worker_threads" +import { pool } from "../db/index.js" import type { CollectorStartPayload, ExporterMapPayload, @@ -21,6 +22,24 @@ let socket: Socket | null = null let flushTimer: ReturnType | null = null let bound = false let address: string | null = null +let shuttingDown = false + +async function shutdown(): Promise { + shuttingDown = true + if (flushTimer) clearInterval(flushTimer) + flushTimer = null + const current = socket + socket = null + bound = false + address = null + if (current) await new Promise((resolve, reject) => { + try { current.close(() => resolve()) } catch (error) { reject(error) } + }) + await flushPending({ force: true, prune: false }) + await pool.end() + send({ type: "shutdown-complete" }) + parentPort?.close() +} function send(msg: WorkerToMain): void { parentPort?.postMessage(msg) @@ -85,6 +104,7 @@ function startListener(payload: CollectorStartPayload): void { heartbeat() }) sock.on("message", (msg, rinfo) => { + if (shuttingDown) return try { ingestDatagram(msg, rinfo.address) } catch (e) { @@ -111,6 +131,14 @@ function startListener(payload: CollectorStartPayload): void { } parentPort?.on("message", (msg: MainToWorker) => { + if (shuttingDown) return + if (msg.type === "shutdown") { + void shutdown().catch(() => { + send({ type: "error", payload: { message: "Не удалось завершить остановку NetFlow worker" } }) + // No acknowledgement: the caller must not start a database command. + }) + return + } try { if (msg.type === "start") startListener(msg.payload) else if (msg.type === "stop") { diff --git a/backend/src/services/traffic-flow-engine.ts b/backend/src/services/traffic-flow-engine.ts index ac0302c2..6da153e6 100644 --- a/backend/src/services/traffic-flow-engine.ts +++ b/backend/src/services/traffic-flow-engine.ts @@ -1,4 +1,5 @@ import { dbAll, dbQuery, pool } from "../db/index.js" +import { createSerialOperation } from "./serial-operation.js" import { ensurePartitionFor, specForParent } from "../db/partitions.js" import { normalizeParsedFlow, parseFlowPacket, protoName, type ParsedFlow, type ParsedFlowInput } from "./traffic-flow-parse.js" import { classifyFlowPlaneLite } from "./traffic-flow-planes.js" @@ -932,7 +933,13 @@ async function upsertFlowBuckets(rows: PendingFlowRow[]): Promise { } } -export async function flushPending(opts?: { force?: boolean; prune?: boolean }): Promise { +const serializeFlush = createSerialOperation() + +export function flushPending(opts?: { force?: boolean; prune?: boolean }): Promise { + return serializeFlush(() => flushPendingInternal(opts)) +} + +async function flushPendingInternal(opts?: { force?: boolean; prune?: boolean }): Promise { pruneRecent() rollFlowRings() const force = Boolean(opts?.force) diff --git a/backend/src/services/traffic-flow-ingest.ts b/backend/src/services/traffic-flow-ingest.ts index 3b7e594f..cb266e68 100644 --- a/backend/src/services/traffic-flow-ingest.ts +++ b/backend/src/services/traffic-flow-ingest.ts @@ -1,6 +1,7 @@ import { deleteTrafficFlowData, executeTrafficFlowPurge } from "./traffic-flow-purge-execution.js" import { withTrafficFlowPurgeLock } from "../db/operation-lock.js" import { Worker } from "node:worker_threads" +import { waitForCollectorShutdown } from "./traffic-flow-worker-shutdown.js" import { db, dbAll, pool } from "../db/index.js" import { dropExpiredPartitions } from "../db/partitions.js" import { servers } from "../db/schema.js" @@ -55,6 +56,7 @@ let restartAttempts = 0 let lastHeartbeat: CollectorHeartbeat | null = null let state: FlowListenerState = { bound: false, address: null } let wantListen = false +let databaseStop: Promise | null = null function workerFileUrl(): URL { const ts = import.meta.url.includes(".ts") @@ -115,10 +117,14 @@ function handleWorkerMessage(msg: WorkerToMain): void { } async function spawnWorker(): Promise { + if (databaseStop) return stopWorkerProcess() const settings = await getTrafficFlowSettingsRow() + if (databaseStop) return configureEngine({ topN: settings.topN, retentionHours: settings.retentionHours }) await applyExporterCtxFromDb() + const exporterMap = await buildExporterMapPayload() + if (databaseStop) return const w = new Worker(workerFileUrl(), { execArgv: process.execArgv }) w.on("message", (msg: WorkerToMain) => handleWorkerMessage(msg)) w.on("error", (err) => { @@ -140,6 +146,7 @@ async function spawnWorker(): Promise { } }) w.on("exit", (code) => { + if (worker !== w) return worker = null state = { bound: false, address: null } if (!wantListen) return @@ -159,7 +166,7 @@ async function spawnWorker(): Promise { listenPort: settings.flowListenPort, topN: settings.topN, retentionHours: settings.retentionHours, - exporterMap: await buildExporterMapPayload(), + exporterMap, }, }) } @@ -255,8 +262,10 @@ export async function getFlowRuntimeCounters() { } export async function startTrafficFlowListener() { + if (databaseStop) throw new Error("Приём NetFlow приостановлен для операции с базой") stopTrafficFlowListener() const settings = await getTrafficFlowSettingsRow() + if (databaseStop) throw new Error("Приём NetFlow приостановлен для операции с базой") if (!settings.enabled) { wantListen = false state = { bound: false, address: null } @@ -273,6 +282,28 @@ export function stopTrafficFlowListener() { state = { bound: false, address: null } } +export async function stopTrafficFlowListenerForDatabaseOperation(): Promise { + if (databaseStop) return databaseStop + wantListen = false + if (restartTimer) clearTimeout(restartTimer) + restartTimer = null + const current = worker + databaseStop = (async () => { + if (current) await waitForCollectorShutdown(current) + await flushPending({ force: true, prune: false }) + state = { bound: false, address: null } + lastHeartbeat = null + })() + // Keep a failed stop latched: restart must not kill a worker whose writes are unconfirmed. + await databaseStop +} + +export async function restartTrafficFlowListenerAfterDatabaseOperation(): Promise { + if (databaseStop) await databaseStop + databaseStop = null + await startTrafficFlowListener() +} + export async function refreshFlowExporterMap(): Promise { await applyExporterCtxFromDb() postToWorker({ type: "updateExporterMap", payload: await buildExporterMapPayload() }) diff --git a/backend/src/services/traffic-flow-worker-shutdown.test.ts b/backend/src/services/traffic-flow-worker-shutdown.test.ts new file mode 100644 index 00000000..666e0b3e --- /dev/null +++ b/backend/src/services/traffic-flow-worker-shutdown.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict" +import { EventEmitter } from "node:events" +import { test } from "node:test" +import { Worker } from "node:worker_threads" +import { waitForCollectorShutdown } from "./traffic-flow-worker-shutdown.js" +import { createSerialOperation } from "./serial-operation.js" + +function fixture() { + const events = new EventEmitter() + let commands = 0 + Object.assign(events, { postMessage: (msg: { type: string }) => { assert.equal(msg.type, "shutdown"); commands++ } }) + return { events, worker: events as unknown as Worker, commands: () => commands } +} +test("acknowledgement alone does not authorize a database operation", async () => { + const f = fixture(); let settled = false + const stop = waitForCollectorShutdown(f.worker).then(() => { settled = true }) + f.events.emit("message", { type: "shutdown-complete" }) + await Promise.resolve(); assert.equal(settled, false) + f.events.emit("exit", 0); await stop + assert.equal(settled, true); assert.equal(f.commands(), 1) + assert.equal(f.events.listenerCount("exit"), 0) +}) +test("unconfirmed exit, error exit and timeout reject without terminating the worker", async () => { + for (const variant of ["unconfirmed", "error", "timeout"]) { + const f = fixture(); const stop = waitForCollectorShutdown(f.worker, 10) + const rejected = assert.rejects(stop, /операция с базой отменена/) + if (variant === "error") f.events.emit("message", { type: "shutdown-complete" }) + if (variant !== "timeout") f.events.emit("exit", variant === "error" ? 1 : 0) + await rejected + assert.equal(f.commands(), 1) + assert.equal(f.events.listenerCount("message"), 0) + } +}) +test("final flush waits for an in-flight flush, even when it rejects", async () => { + const serial = createSerialOperation(); const calls: string[] = [] + let release!: () => void + const gate = new Promise(resolve => { release = resolve }) + const first = serial(async () => { calls.push("first"); await gate; throw Error("write failed") }) + const rejected = assert.rejects(first, /write failed/) + const final = serial(async () => { calls.push("final") }) + await Promise.resolve(); assert.deepEqual(calls, ["first"]) + release(); await rejected; await final + assert.deepEqual(calls, ["first", "final"]) +}) + +test("a real worker acknowledges shutdown and exits without forced termination", async () => { + const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + parentPort.on('message', msg => { + if (msg.type === 'shutdown') { + parentPort.postMessage({ type: 'shutdown-complete' }); + parentPort.close(); + } + }); + `, { eval: true }) + try { await waitForCollectorShutdown(worker, 5_000) } + finally { await worker.terminate() } +}) diff --git a/backend/src/services/traffic-flow-worker-shutdown.ts b/backend/src/services/traffic-flow-worker-shutdown.ts new file mode 100644 index 00000000..6680fc2a --- /dev/null +++ b/backend/src/services/traffic-flow-worker-shutdown.ts @@ -0,0 +1,22 @@ +import type { Worker } from "node:worker_threads" +import type { WorkerToMain } from "./traffic-flow-collector-ipc.js" + +/** Both an acknowledgement after pool.end() and a clean exit are required. */ +export function waitForCollectorShutdown(worker: Worker, timeoutMs = 30_000): Promise { + return new Promise((resolve, reject) => { + let acknowledged = false + const cleanup = () => { + clearTimeout(timer) + worker.off("message", message); worker.off("exit", exit); worker.off("error", error) + } + const error = () => { cleanup(); reject(new Error("Остановка NetFlow worker не подтверждена; операция с базой отменена")) } + const message = (msg: WorkerToMain) => { if (msg.type === "shutdown-complete") acknowledged = true } + const exit = (code: number) => { + if (!acknowledged || code !== 0) return error() + cleanup(); resolve() + } + const timer = setTimeout(error, timeoutMs) + worker.on("message", message); worker.on("exit", exit); worker.on("error", error) + try { worker.postMessage({ type: "shutdown" }) } catch { error() } + }) +} diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index bd9c0451..f19bf435 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -25,7 +25,7 @@ Приложение использует pg_dump -Fc и pg_restore --single-transaction --clean --if-exists. Транзакционный режим описан в [документации PostgreSQL](https://www.postgresql.org/docs/current/app-pgrestore.html). Проверка пяти байтов PGDMP проверяет заголовок, но не полноту и не доверие к архиву. Для rollback нужен сценарий с ошибкой исполнения restore, а не только отказ HTTP до его начала. -В локальном блоке12.5.8 добавлено ожидание текущих scheduler jobs с закрытием admission, но этот код ещё не развёрнут на стенде673e5ba. Локальный вызов stopTrafficFlowListener пока не подтверждает завершение worker/flush; ручные API writers и экземпляры на других хостах также требуют отдельного контроля. PG-6 остаётся открытым до наблюдаемого подтверждения; восстановление стенда выполняется после его получения. Реальные тесты backend, способные очищать таблицы, запускаются только против выделенной тестовой БД. +В локальном блоке12.5.8 добавлено ожидание текущих scheduler jobs с закрытием admission, но этот код ещё не развёрнут на стенде673e5ba. Блок12.5.9 добавляет отдельный graceful shutdown worker для database operation, serial flush, pool.end и обязательные acknowledgement/exit0. Локальные protocol tests прошли, но настоящий collector/UDP/длительный SQL ещё не проверен и код не развёрнут. Ручные API writers, вспомогательные writers и экземпляры на других хостах также требуют отдельного контроля. PG-6 остаётся открытым до наблюдаемого подтверждения; восстановление стенда выполняется после его получения. Реальные тесты backend, способные очищать таблицы, запускаются только против выделенной тестовой БД. После bootstrap достаточно следующих запросов чтения для начальной проверки, без вывода секретов: diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 2985aba4..1772e66d 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-07: блок 12.5.8 завершён в части scheduler lifecycle. Операции с БД закрывают новые запуски и ждут текущие jobs; таймаут отменяет команду, ручной запуск сообщает точный409. Локальные профильные проверки прошли. Изменения ещё не развёрнуты на стенде673e5ba; PG-6 остаётся открытым. Следующий 12.5.9 — graceful stop/flush NetFlow worker и проверка lifecycle перед restore. +Актуальное продолжение 2026-10-07: блок12.5.9 завершён локально — отдельный shutdown NetFlow закрывает UDP, сериализует flush, закрывает worker PG pool и требует acknowledgement плюс штатный exit. Четыре lifecycle tests, database HTTP/JWT и build прошли. На стенде остаётся source673e5ba. Следующий12.5.10 — реальные worker/UDP/длительная запись в выделенной проверочной БД и контроль остальных writers; PG-6/restore остаются открытыми. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-07. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ab661cb5..7a854a72 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-07: блок 12.5.8 завершён в части scheduler lifecycle. Операции с БД закрывают новые запуски и ждут текущие jobs; таймаут отменяет команду, ручной запуск сообщает точный409. Локальные профильные проверки прошли. Изменения ещё не развёрнуты на стенде673e5ba; PG-6 остаётся открытым. Следующий 12.5.9 — graceful stop/flush NetFlow worker и проверка lifecycle перед restore. +Актуальное продолжение 2026-10-07: блок12.5.9 завершён локально — отдельный shutdown NetFlow закрывает UDP, сериализует flush, закрывает worker PG pool и требует acknowledgement плюс штатный exit. Четыре lifecycle tests, database HTTP/JWT и build прошли. На стенде остаётся source673e5ba. Следующий12.5.10 — реальные worker/UDP/длительная запись в выделенной проверочной БД и контроль остальных writers; PG-6/restore остаются открытыми. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -34,6 +34,16 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в описанных границах:12.3.83,main1186/1186,context221/221. Этап остаётся в работе: стенд12.5.1–3 развёрнут,bootstrap/smoke проверены;остальные сценарии12.5 не выполнены. | +## 12.5.9 — подтверждённая остановка NetFlow worker перед операцией с БД (2026-10-07) + +Добавлена отдельная IPC команда shutdown для database operation; обычный stop остальных потребителей не подменён. Worker прекращает приём datagrams, закрывает UDP socket/таймер, ждёт принудительный flush без prune и pool.end(), затем отправляет shutdown-complete и закрывает parentPort. Основной процесс требует и acknowledgement, и exit0. Timeout30сек, неподтверждённый exit и ошибки отменяют database operation без принудительного terminate. Неуспешный stop остаётся latched: попытка restart не уничтожает worker с неподтверждёнными записями и возвращает существующее listenerError предупреждение. + +Flush engine сериализован общей очередью: завершающий flush не обгоняет уже выполняющуюся запись; отказ одного вызова не ломает следующий. По окончании worker дополнительно ожидается очередь main engine. Listener admission закрыт на весь database operation: обычный start во время паузы отклонён; явный restartAfterDatabaseOperation снимает паузу только после успешного stop. Асинхронный spawn проверяет паузу после чтения контекста, поздний exit старого worker не сбрасывает ссылку на новый. system-database-backup подключён к отдельной паре stop/restart. + +Проверки: четыре новых pure/protocol tests прошли — ack без exit недостаточен, exit без ack/exit1/timeout отменяют операцию, final flush ждёт in-flight rejected flush, настоящий worker thread подтверждает shutdown и штатно выходит. Это worker fixture для транспорта, не настоящий UDP collector с PostgreSQL. Новый тест включён в test:system-database. Существующие7pure database tests, HTTP/JWT scripts прошли; contracts/backend build после окончательных изменений passed; diff-check passed. Тесты с очисткой БД не запускались. + +PG-6 ещё открыт: тест с реальным collector/UDP и длительным SQL, конкурирующие ручные API writers, вспомогательные writers и другие экземпляры не проверены. flushPending сохраняет прежнюю best-effort обработку ошибок отдельных хранилищ: подтверждение означает завершение попыток записи/закрытие pool, а не гарантию сохранения каждого packet. Новый код пока не развёрнут, активный source673e5ba; данные стенда/RouterOS не менялись, API restore основной БД не запускался. Следующий12.5.10 — запуск текущего collector в выделенной mmapp_acceptance_20261007, наблюдение UDP/SQL drain и безопасный контроль остальных writers перед развёртыванием/PG-3/4. Локальный коммит без push. + ## 12.5.8 — ожидание заданий планировщика перед операцией с БД (2026-10-07) Исправлена первая часть обнаруженного в12.5.7 lifecycle: scheduler-running закрывает admission до ожидания всех активных jobs. Слот освобождается в существующем finally после collector и записи scheduler journal. Ожидание ограничено30сек; при таймауте pg_dump/pg_restore не запускается, исходная ошибка сохраняется, consumers проходят стандартную попытку перезапуска. Повторный запуск уже выполняющейся job по-прежнему отклонён. system-database-backup подключён к новым stopSchedulerForDatabaseOperation/restartSchedulerAfterDatabaseOperation; обычный stopScheduler для существующих потребителей сохраняет синхронный контракт. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 2e19e9eb..edc678a2 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-07: блок 12.5.8 завершён в части scheduler lifecycle. Операции с БД закрывают новые запуски и ждут текущие jobs; таймаут отменяет команду, ручной запуск сообщает точный409. Локальные профильные проверки прошли. Изменения ещё не развёрнуты на стенде673e5ba; PG-6 остаётся открытым. Следующий 12.5.9 — graceful stop/flush NetFlow worker и проверка lifecycle перед restore. +Актуальное продолжение 2026-10-07: блок12.5.9 завершён локально — отдельный shutdown NetFlow закрывает UDP, сериализует flush, закрывает worker PG pool и требует acknowledgement плюс штатный exit. Четыре lifecycle tests, database HTTP/JWT и build прошли. На стенде остаётся source673e5ba. Следующий12.5.10 — реальные worker/UDP/длительная запись в выделенной проверочной БД и контроль остальных writers; PG-6/restore остаются открытыми. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -1020,3 +1020,13 @@ PG-5: отдельная SQL-транзакция временно удержи Изменены scheduler-running.ts, scheduler.ts, alert-collector-hooks.ts, system-database-backup.ts и scheduler route; новый lifecycle test включён в test:scheduler. Проверки: test:scheduler17/17 (3lifecycle,9recorded-run,5HTTP), test:system-database (7pure tests и HTTP/JWT scripts) passed; contracts/backend build passed. Проверены два одновременно активных jobs/отказ нового запуска, сохранение паузы до завершения, timeout с нулём вызовов database command, recovery, queued alert hook и HTTP409 с настоящим manual runner. Эти проверки не требуют native RouterOS apply или очистки тестовой БД. Это локальный кодовый блок, не завершение PG-6. NetFlow ещё использует fire-and-forget flush/terminate; прочие ручные API writers и другие экземпляры не охвачены scheduler gate. Активный Docker source остаётся673e5ba, новый код пока не развёрнут. Восстановление основной тестовой БД не запускалось; данные/CHR/серверный auth режим не менялись. Следующий 12.5.9 — graceful stop с подтверждением завершения worker flush и выхода worker, затем сборка/развёртывание и наблюдаемая PG-6 приёмка. Локальный коммит без push. + +## 12.5.9 — подтверждённая остановка NetFlow worker перед операцией с БД (2026-10-07) + +Добавлена отдельная IPC команда shutdown для database operation; обычный stop остальных потребителей не подменён. Worker прекращает приём datagrams, закрывает UDP socket/таймер, ждёт принудительный flush без prune и pool.end(), затем отправляет shutdown-complete и закрывает parentPort. Основной процесс требует и acknowledgement, и exit0. Timeout30сек, неподтверждённый exit и ошибки отменяют database operation без принудительного terminate. Неуспешный stop остаётся latched: попытка restart не уничтожает worker с неподтверждёнными записями и возвращает существующее listenerError предупреждение. + +Flush engine сериализован общей очередью: завершающий flush не обгоняет уже выполняющуюся запись; отказ одного вызова не ломает следующий. По окончании worker дополнительно ожидается очередь main engine. Listener admission закрыт на весь database operation: обычный start во время паузы отклонён; явный restartAfterDatabaseOperation снимает паузу только после успешного stop. Асинхронный spawn проверяет паузу после чтения контекста, поздний exit старого worker не сбрасывает ссылку на новый. system-database-backup подключён к отдельной паре stop/restart. + +Проверки: четыре новых pure/protocol tests прошли — ack без exit недостаточен, exit без ack/exit1/timeout отменяют операцию, final flush ждёт in-flight rejected flush, настоящий worker thread подтверждает shutdown и штатно выходит. Это worker fixture для транспорта, не настоящий UDP collector с PostgreSQL. Новый тест включён в test:system-database. Существующие7pure database tests, HTTP/JWT scripts прошли; contracts/backend build после окончательных изменений passed; diff-check passed. Тесты с очисткой БД не запускались. + +PG-6 ещё открыт: тест с реальным collector/UDP и длительным SQL, конкурирующие ручные API writers, вспомогательные writers и другие экземпляры не проверены. flushPending сохраняет прежнюю best-effort обработку ошибок отдельных хранилищ: подтверждение означает завершение попыток записи/закрытие pool, а не гарантию сохранения каждого packet. Новый код пока не развёрнут, активный source673e5ba; данные стенда/RouterOS не менялись, API restore основной БД не запускался. Следующий12.5.10 — запуск текущего collector в выделенной mmapp_acceptance_20261007, наблюдение UDP/SQL drain и безопасный контроль остальных writers перед развёртыванием/PG-3/4. Локальный коммит без push. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 1356fcd1..e0e0b88d 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -77,3 +77,7 @@ ssh -N -L 3000:127.0.0.1:3000 -L 8000:127.0.0.1:8000 root@161.104.46.222 ## Локальный lifecycle patch — 2026-10-07 Блок12.5.8 исправляет ожидание scheduler jobs и запрещает новые запуски во время database operation;17scheduler tests,7pure database tests,HTTP/JWT scripts и build прошли. На этом стенде patch пока не развёрнут: активный source673e5ba сохраняется. Следующий12.5.9 — graceful NetFlow worker stop/flush; только после этого перейти к сборке/развёртыванию и внешней проверкеPG-6. Восстановление основной тестовой БД ещё не выполнялось. + +## Локальный NetFlow shutdown patch — 2026-10-07 + +В12.5.9 добавлена командаshutdown с подтверждением послеflush/pool.end и ожиданиемexit0;4pure/protocol tests, database HTTP/JWT и build passed. На стенд ещё не развёрнуто, source673e5ba остаётся активным. Реальный collector/UDP/длительный SQL и прочие writers требуют отдельной проверки12.5.10 в mmapp_acceptance_20261007. Подтверждение завершения flush не гарантирует сохранение всех пакетов при прежних best-effort ошибках engine; PG-6/restore не приняты.