fix(api): подтвердить остановку NetFlow перед операцией с БД
This commit is contained in:
1 parent
1b89d56ea3
commit
f8da3e08a0
14 files changed
+192
-11
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -77,3 +77,7 @@ ssh -N -L 3000:127.0.0.1:3000 -L 8000:127.0.0.1:8000 [email protected]
|
||||
## Локальный 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 не приняты.
|
||||
Reference in new issue
Block a user