fix(api): подтвердить остановку NetFlow перед операцией с БД

This commit is contained in:
Denozordec committed 2026-10-07 22:48:19 +07:00
1 parent 1b89d56ea3
commit f8da3e08a0
14 files changed
+192 -11

No files matched your search

+1 -1
View File
@@ -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",
+9
View File
@@ -0,0 +1,9 @@
/** Each caller observes its own result; a rejection does not break the queue. */
export function createSerialOperation() {
let tail: Promise<unknown> = Promise.resolve()
return <T>(run: () => Promise<T>): Promise<T> => {
const result = tail.then(run, run)
tail = result.catch(() => {})
return result
}
}
@@ -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<T extends object>(fn: () => Promise<T>): Promise<T & DatabaseOperationWarnings> {
return withOperationLock(() => pool.connect(), BACKUP_LOCK_KEY, () => executeDatabaseOperation(fn, {
stopListener: stopTrafficFlowListener,
stopListener: stopTrafficFlowListenerForDatabaseOperation,
stopScheduler: stopSchedulerForDatabaseOperation,
startListener: startTrafficFlowListener,
startListener: restartTrafficFlowListenerAfterDatabaseOperation,
startScheduler: restartSchedulerAfterDatabaseOperation,
}), "Операция с базой приложения уже выполняется")
}
@@ -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" }
@@ -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<typeof setInterval> | null = null
let bound = false
let address: string | null = null
let shuttingDown = false
async function shutdown(): Promise<void> {
shuttingDown = true
if (flushTimer) clearInterval(flushTimer)
flushTimer = null
const current = socket
socket = null
bound = false
address = null
if (current) await new Promise<void>((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") {
+8 -1
View File
@@ -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<number> {
}
}
export async function flushPending(opts?: { force?: boolean; prune?: boolean }): Promise<void> {
const serializeFlush = createSerialOperation()
export function flushPending(opts?: { force?: boolean; prune?: boolean }): Promise<void> {
return serializeFlush(() => flushPendingInternal(opts))
}
async function flushPendingInternal(opts?: { force?: boolean; prune?: boolean }): Promise<void> {
pruneRecent()
rollFlowRings()
const force = Boolean(opts?.force)
+32 -1
View File
@@ -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<void> | 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<void> {
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<void> {
}
})
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<void> {
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<void> {
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<void> {
if (databaseStop) await databaseStop
databaseStop = null
await startTrafficFlowListener()
}
export async function refreshFlowExporterMap(): Promise<void> {
await applyExporterCtxFromDb()
postToWorker({ type: "updateExporterMap", payload: await buildExporterMapPayload() })
@@ -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<void>(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() }
})
@@ -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<void> {
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() }
})
}
+1 -1
View File
@@ -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 достаточно следующих запросов чтения для начальной проверки, без вывода секретов:
+1 -1
View File
@@ -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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+11 -1
View File
@@ -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 для существующих потребителей сохраняет синхронный контракт.
+11 -1
View File
@@ -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.
+4
View File
@@ -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 не приняты.