fix(api): дождаться фоновых записей перед операцией с БД

This commit is contained in:
Denozordec committed 2026-10-07 23:07:10 +07:00
1 parent 8682f2a7a5
commit 2943032462
18 files changed
+183 -29

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/database-request-gate.test.ts && 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"
"test:system-database": "tsx src/services/database-background-work.test.ts && tsx src/services/database-request-gate.test.ts && 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",
+8 -7
View File
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "../services/database-background-work.js"
import { randomUUID } from "node:crypto"
import { z } from "zod"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
@@ -122,7 +123,7 @@ export function createBackupsRoutes(deps: { listServers?: () => Promise<Array<{
return reply.status(202).send({ jobId, status: raced.status, total: raced.total, completed: raced.completed, serverIds: raced.serverIds })
}
backupJobs.set(jobId, job)
void event({
void databaseBackgroundWork.track(event({
level: "info",
eventType: "backups.job.started",
sourceModule: "backups",
@@ -134,9 +135,9 @@ export function createBackupsRoutes(deps: { listServers?: () => Promise<Array<{
serverIds: ids,
notes: notes ?? null,
},
})
queueMicrotask(async () => {
void processBackupJob(job, ids, notes, { create: deps.create ?? runBackupForServer, event }).catch(async (err) => {
})).catch(() => {})
void databaseBackgroundWork.track(Promise.resolve().then(async () => {
await processBackupJob(job, ids, notes, { create: deps.create ?? runBackupForServer, event }).catch(async (err) => {
job.status = "failed"
job.finishedAt = new Date().toISOString()
const failure = {
@@ -158,7 +159,7 @@ export function createBackupsRoutes(deps: { listServers?: () => Promise<Array<{
},
})
})
})
})).catch(() => {})
return reply.status(202).send({
jobId,
status: job.status,
@@ -190,7 +191,7 @@ export function createBackupsRoutes(deps: { listServers?: () => Promise<Array<{
app.post("/backups/:id/restore", { schema: { params: BackupIdParamSchema } }, async (req, reply) => {
try {
const result = await (deps.restore ?? restoreBackupToDevice)(req.params.id)
void event({
void databaseBackgroundWork.track(event({
level: "warning",
eventType: "backups.restore",
sourceModule: "backups",
@@ -198,7 +199,7 @@ export function createBackupsRoutes(deps: { listServers?: () => Promise<Array<{
message: `${result.filename} → ${result.serverName}`,
entityType: "backup",
entityId: req.params.id,
})
})).catch(() => {})
return reply.send(result)
} catch (err) {
return reply.status(err instanceof BackupRestoreUnknownError ? 502 : 400).send({
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "./database-background-work.js"
import { certificateIssueStepSchema } from "@mmapp/contracts/certificates"
import type { CertificateIssueJobRow } from "../db/schema.js"
import type { appendEvent } from "../modules/events/service/events-service.js"
@@ -70,7 +71,7 @@ export function createCertificateIssueRunner<S extends { id: number; name: strin
} finally { runningJobs.delete(jobId) }
}
function queue(jobId: string): void {
queueMicrotask(() => { void run(jobId).catch(report) })
void databaseBackgroundWork.track(Promise.resolve().then(() => run(jobId))).catch(report)
}
return { run, queue }
}
@@ -0,0 +1,62 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { DatabaseBackgroundWork, databaseBackgroundWork } from "./database-background-work.js"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import { createBackupsRoutes } from "../routes/backups.js"
function deferred() {
let resolve!: () => void
const promise = new Promise<void>(r => { resolve = r })
return { promise, resolve }
}
test("drain includes writes scheduled by a task after its HTTP response", async () => {
const work = new DatabaseBackgroundWork(), parent = deferred(), child = deferred()
work.track(parent.promise.then(() => { work.track(child.promise) }))
let finished = false
const drain = work.drain().then(() => { finished = true })
parent.resolve(); await parent.promise; await Promise.resolve()
assert.equal(finished, false)
child.resolve(); await drain; assert.equal(finished, true)
})
test("timeout preserves pending tasks for the next attempt", async () => {
const work = new DatabaseBackgroundWork(), pending = deferred()
work.track(pending.promise)
await assert.rejects(work.drain(10), /операция с базой отменена/)
let finished = false
const retry = work.drain().then(() => { finished = true })
await Promise.resolve(); assert.equal(finished, false)
pending.resolve(); await retry; assert.equal(finished, true)
})
test("a task rejection is observed without breaking subsequent drain", async () => {
const work = new DatabaseBackgroundWork()
const task = work.track(Promise.reject(Error("audit failed")))
await assert.rejects(task, /audit failed/)
await work.drain()
work.track(Promise.resolve()); await work.drain()
})
test("real backup HTTP202 keeps its detached job registered through completion", async () => {
const pending = deferred(), started = deferred()
const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
await app.register(createBackupsRoutes({
listServers: async () => [{ id: 1 }],
appendEvent: async () => undefined as never,
create: async () => {
started.resolve(); await pending.promise
return { id: "drain-backup", serverId: 1, serverName: "router", filename: "test.rsc", sizeBytes: 0,
createdAt: "2026-10-07T00:00:00Z", kind: "manual", storage: "local" }
},
}), { prefix: "/api" })
try {
assert.equal((await app.inject({ method: "POST", url: "/api/backups/create", payload: { serverIds: [1] } })).statusCode, 202)
await started.promise
let finished = false
const drain = databaseBackgroundWork.drain().then(() => { finished = true })
await Promise.resolve(); assert.equal(finished, false)
pending.resolve(); await drain; assert.equal(finished, true)
} finally { pending.resolve(); await databaseBackgroundWork.drain(); await app.close() }
})
@@ -0,0 +1,22 @@
/** Includes child writes registered by tasks that are already running. */
export class DatabaseBackgroundWork {
private pending = new Set<Promise<unknown>>()
track<T>(task: Promise<T>): Promise<T> {
this.pending.add(task)
void task.then(() => this.pending.delete(task), () => this.pending.delete(task))
return task
}
async drain(timeoutMs = 30_000): Promise<void> {
let timer: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error("Не удалось дождаться фоновых записей; операция с базой отменена")), timeoutMs)
})
try {
while (this.pending.size) await Promise.race([Promise.allSettled([...this.pending]), timeout])
} finally { if (timer) clearTimeout(timer) }
}
}
export const databaseBackgroundWork = new DatabaseBackgroundWork()
+2 -1
View File
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "./database-background-work.js"
import { eq } from "drizzle-orm"
import { db, dbQuery, pool } from "../db/index.js"
import { dropExpiredPartitions, ensurePartitionsAround } from "../db/partitions.js"
@@ -91,7 +92,7 @@ export async function pollServer(serverId: number): Promise<SnapshotRead> {
[serverId, inserted.polledAt],
)
}
void dropExpiredPartitions(pool).then(() => ensurePartitionsAround(pool))
void databaseBackgroundWork.track(dropExpiredPartitions(pool).then(() => ensurePartitionsAround(pool))).catch(() => {})
return toSnapshotRead(inserted!)
}
@@ -9,6 +9,7 @@ import { pool } from "../db/index.js"
import { withOperationLock } from "../db/operation-lock.js"
import { executeDatabaseOperation, type DatabaseOperationWarnings } from "./system-database-operation.js"
import { restartSchedulerAfterDatabaseOperation, stopSchedulerForDatabaseOperation } from "./scheduler.js"
import { pauseRipeBackgroundWork, resumeRipeBackgroundWork } from "./traffic-flow-ripe.js"
import {
restartTrafficFlowListenerAfterDatabaseOperation,
stopTrafficFlowListenerForDatabaseOperation,
@@ -49,9 +50,15 @@ 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: stopTrafficFlowListenerForDatabaseOperation,
stopScheduler: stopSchedulerForDatabaseOperation,
stopScheduler: async () => {
await stopSchedulerForDatabaseOperation()
await pauseRipeBackgroundWork()
},
startListener: restartTrafficFlowListenerAfterDatabaseOperation,
startScheduler: restartSchedulerAfterDatabaseOperation,
startScheduler: async () => {
resumeRipeBackgroundWork()
await restartSchedulerAfterDatabaseOperation()
},
}), "Операция с базой приложения уже выполняется")
}
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "./database-background-work.js"
import { OTHER_SERVICE, resolveFlowBrand } from "./traffic-flow-brands.js"
import { db } from "../db/index.js"
import { evobgpSettings } from "../db/schema.js"
@@ -149,5 +150,5 @@ async function fetchCatalog(): Promise<void> {
/** Background refresh — analytics never awaits the HTTP. */
export function refreshFlowCatalogInBackground(): void {
void fetchCatalog()
void databaseBackgroundWork.track(fetchCatalog())
}
@@ -1,6 +1,7 @@
import { createSocket, type Socket } from "node:dgram"
import { parentPort } from "node:worker_threads"
import { pool } from "../db/index.js"
import { pauseRipeBackgroundWork } from "./traffic-flow-ripe.js"
import type {
CollectorStartPayload,
ExporterMapPayload,
@@ -36,6 +37,7 @@ async function shutdown(): Promise<void> {
try { current.close(() => resolve()) } catch (error) { reject(error) }
})
await flushPending({ force: true, prune: false })
await pauseRipeBackgroundWork()
await pool.end()
send({ type: "shutdown-complete" })
parentPort?.close()
+3 -2
View File
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "./database-background-work.js"
import { dbAll, dbQuery, pool } from "../db/index.js"
import { createSerialOperation } from "./serial-operation.js"
import { ensurePartitionFor, specForParent } from "../db/partitions.js"
@@ -369,8 +370,8 @@ export function queueParsedFlows(serverId: number, flows: ParsedFlowInput[]): vo
const ripeMisses: string[] = []
const topo = peekFlowTopology()
const catalog = peekServerCatalog()
if (!topo) void loadFlowTopology().catch(() => {})
if (!catalog) void getServerCatalog().catch(() => {})
if (!topo) void databaseBackgroundWork.track(loadFlowTopology()).catch(() => {})
if (!catalog) void databaseBackgroundWork.track(getServerCatalog()).catch(() => {})
const serverType = catalog?.byId.get(serverId)?.type
for (const raw of flows) {
const flow = normalizeParsedFlow(raw)
+2 -1
View File
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "./database-background-work.js"
import { eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
@@ -47,7 +48,7 @@ export async function refreshServerIfaces(serverId: number, force = false): Prom
export function maybeRefreshIfaces(serverId: number): boolean {
if (!shouldRefreshIfaces(serverId)) return false
void refreshIfacesImpl(serverId)
void databaseBackgroundWork.track(refreshIfacesImpl(serverId)).catch(() => {})
return true
}
+15 -1
View File
@@ -10,6 +10,8 @@ import {
ripeLastCandidateCountForTests,
seedRipeCacheForTests,
setRipeFetchForTests,
pauseRipeBackgroundWork,
resumeRipeBackgroundWork,
} from "./traffic-flow-ripe.js"
disableRipePersistForTests()
@@ -133,4 +135,16 @@ assert.ok(
`index should not scan all prefixes, got ${ripeLastCandidateCountForTests()}`,
)
console.log("traffic-flow-ripe.test.ts: ok")
resetRipeCacheForTests()
disableRipePersistForTests()
setRipeFetchForTests(async () => new Response(JSON.stringify({ data: {} }), { status: 200 }))
await pauseRipeBackgroundWork()
enqueueRipeMisses(["8.8.4.4"])
await flushRipeQueueForTests()
assert.equal(ripeFetchCountForTests(), 0)
resumeRipeBackgroundWork()
enqueueRipeMisses(["8.8.4.4"])
await flushRipeQueueForTests()
assert.ok(ripeFetchCountForTests() > 0)
console.log("traffic-flow-ripe.test.ts: ok (including database pause/resume)")
+21 -8
View File
@@ -1,3 +1,4 @@
import { databaseBackgroundWork } from "./database-background-work.js"
import { dbAll, dbQuery } from "../db/index.js"
import { canonicalIp, ipv4ToInt, isNonPublicIp, parseCidrV4 } from "./traffic-flow-ip.js"
import { resolveRipeCountry } from "./traffic-flow-brands.js"
@@ -26,6 +27,7 @@ const asnHolder = new Map<number, { holder: string; fetchedAt: number }>()
const inflight = new Map<string, Promise<FlowIpMeta | null>>()
const queue: string[] = []
const queued = new Set<string>()
let databasePaused = false
const recentFetches: number[] = []
interface RipeIndexed {
@@ -155,7 +157,7 @@ function loadSqlite(): void {
return
}
loaded = true
void loadPg()
void databaseBackgroundWork.track(loadPg())
}
async function loadPg(): Promise<void> {
@@ -204,7 +206,7 @@ async function loadPg(): Promise<void> {
function persist(entry: FlowIpMeta): void {
if (!persistEnabled) return
void dbQuery(`
void databaseBackgroundWork.track(dbQuery(`
INSERT INTO flow_ip_meta (prefix, asn, country, lat, lng, holder, ok, fetched_at)
VALUES (@prefix, @asn, @country, @lat, @lng, @holder, @ok, @fetchedAt)
ON CONFLICT(prefix) DO UPDATE SET
@@ -219,12 +221,12 @@ function persist(entry: FlowIpMeta): void {
holder: entry.holder,
ok: entry.ok ? 1 : 0,
fetchedAt: new Date(entry.fetchedAt).toISOString(),
}).catch(() => { /* ignore persist errors */ })
})).catch(() => { /* ignore persist errors */ })
}
function persistAsn(asn: number, holder: string): void {
if (!persistEnabled || !asn) return
void dbQuery(`
void databaseBackgroundWork.track(dbQuery(`
INSERT INTO flow_asn_meta (asn, holder, fetched_at)
VALUES (@asn, @holder, @fetchedAt)
ON CONFLICT(asn) DO UPDATE SET holder=excluded.holder, fetched_at=excluded.fetched_at
@@ -232,7 +234,7 @@ function persistAsn(asn: number, holder: string): void {
asn,
holder,
fetchedAt: new Date().toISOString(),
}).catch(() => { /* ignore */ })
})).catch(() => { /* ignore */ })
}
/** Удаляет просроченный RIPE-кэш с диска (hit 24h / negative 6h). */
@@ -434,13 +436,13 @@ async function runWorker(): Promise<void> {
}
} finally {
workerRunning = false
if (queue.length) void runWorker()
if (queue.length) void databaseBackgroundWork.track(runWorker())
}
}
/** HTTP / SSE never await this — cache miss is filled on a later tick. */
export function enqueueRipeMisses(ips: Iterable<string>): void {
if (!enqueueEnabled) return
if (!enqueueEnabled || databasePaused) return
loadSqlite()
for (const raw of ips) {
if (queue.length >= MAX_QUEUE) break
@@ -451,5 +453,16 @@ export function enqueueRipeMisses(ips: Iterable<string>): void {
queued.add(ip)
queue.push(ip)
}
if (queue.length) void runWorker()
if (queue.length) void databaseBackgroundWork.track(runWorker())
}
export async function pauseRipeBackgroundWork(): Promise<void> {
databasePaused = true
queue.length = 0
queued.clear()
await databaseBackgroundWork.drain()
}
export function resumeRipeBackgroundWork(): void {
databasePaused = false
}
+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. Блок12.5.9 добавляет отдельный graceful shutdown worker для database operation, serial flush, pool.end и обязательные acknowledgement/exit0. Локальные protocol tests и настоящий isolated collector/UDP/blocked SQL drain12.5.10 прошли: [evidence](ux-ui/acceptance/netflow-worker-2026-10-07.json). Основной код на стенде ещё не обновлён; app-wide quiescence этим не подтверждена. Блок12.5.11 локально добавляет admission/drain всех API handlers иtraffic live streams; новые tests/build passed, на стенде patch не развёрнут. Detached backup jobs/audit иRIPE background persistence ещё неохвачены и требуют12.5.12. Другие экземпляры/хосты требуют отдельного контроля. 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 и настоящий isolated collector/UDP/blocked SQL drain12.5.10 прошли: [evidence](ux-ui/acceptance/netflow-worker-2026-10-07.json). Основной код на стенде ещё не обновлён; app-wide quiescence этим не подтверждена. Блок12.5.11 локально добавляет admission/drain всех API handlers иtraffic live streams; новые tests/build passed, на стенде patch не развёрнут. В12.5.12 локально добавлен общий background registry/drain дляbackup/audit/certificate/partition maintenance/cache work, атакжеpause RIPE enqueue. Tests/build passed; всяцепочка требуетintegrated quiescence12.5.13 иcache invalidation передrestore. Основнойстенд покаsource673e5ba. Другие экземпляры/хосты требуют отдельного контроля. PG-6 остаётся открытым до наблюдаемого подтверждения; восстановление стенда выполняется после его получения. Реальные тесты backend, способные очищать таблицы, запускаются только против выделенной тестовой БД.
После bootstrap достаточно следующих запросов чтения для начальной проверки, без вывода секретов:
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-07:12.5.11 завершён локально для HTTP handlers — backup/restore закрывают API admission, ждут завершения обработчиков и останавливают traffic live-подписки с ожиданиемcleanup. Четыре новых теста,15lifecycle/database tests,HTTP/JWT scripts иbuild passed. Основной runtime673e5ba не обновлён. Следующий12.5.12 — detached backup jobs/audit иRIPE persistence; PG-6/restore остаются открытыми.
Актуальное продолжение 2026-10-07:12.5.12 завершён локально — учтены detached backup jobs/audit, выпуск сертификатов, poll partition maintenance иbackground cache work; RIPE admission закрывается перед drain.19lifecycle/database tests,HTTP/JWT,backup/RIPE,10certificate tests иbuild passed. Основной runtime673e5ba не обновлён. Следующий12.5.13 — integrated HTTP/scheduler/background/UDP quiescence ввыделеннойБД, cache invalidation послеrestore иdeployment; PG-6/PG-3/4 ещёоткрыты.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-07. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+13 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-07:12.5.11 завершён локально для HTTP handlers — backup/restore закрывают API admission, ждут завершения обработчиков и останавливают traffic live-подписки с ожиданиемcleanup. Четыре новых теста,15lifecycle/database tests,HTTP/JWT scripts иbuild passed. Основной runtime673e5ba не обновлён. Следующий12.5.12 — detached backup jobs/audit иRIPE persistence; PG-6/restore остаются открытыми.
Актуальное продолжение 2026-10-07:12.5.12 завершён локально — учтены detached backup jobs/audit, выпуск сертификатов, poll partition maintenance иbackground cache work; RIPE admission закрывается перед drain.19lifecycle/database tests,HTTP/JWT,backup/RIPE,10certificate tests иbuild passed. Основной runtime673e5ba не обновлён. Следующий12.5.13 — integrated HTTP/scheduler/background/UDP quiescence ввыделеннойБД, cache invalidation послеrestore иdeployment; PG-6/PG-3/4 ещёоткрыты.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -34,6 +34,18 @@
| 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.12 — ожидание фоновых jobs, audit и записей RIPE (2026-10-07)
Добавлен общий process-local DatabaseBackgroundWork. Promise регистрируется до запуска отложенного тела; drain ждёт settlement всех задач, включая дочерние записи, которые регистрируются во время завершения parent task. Timeout30сек отменяет database operation; pending registry сохраняется для следующего drain. Rejection наблюдается отдельно и не ломает ожидание остальных задач; завершение drain не объявляет неуспешные записи успешными.
Backups HTTP202 теперь регистрирует job до microtask запуска и держит его учтённым до processBackupJob/terminal audit. Started/restore audit promises также учтены; ошибки audit не подменяют подтверждённую операцию. Выпуск сертификата изqueue учтён доprogress/terminal state/event. Опрос сервера учитывает detached dropExpiredPartitions/ensurePartitionsAround (DDL особенно опасен во времяrestore). Фоновые загрузки topology/catalog/интерфейсов/classification catalog также учтены, хотя часть изних только читаетБД/обновляетmemory cache.
RIPE учитывает loadPg, resolver worker иотдельные persist/persistAsn promises. Pause закрывает admission cache misses иочищает ещёневыданную best-effort enrichment очередь; текущие resolver иихSQL writes ожидаются. ПослеstopListener иscheduler drain database operation ждёт общий background registry; стандартный restart вновьразрешаетRIPE. Настоящий UDP worker передpool.end также вызываетpause/drain, поэтомуизолированные auxiliary writes не могут остаться заегоподтверждённымвыходом. Большая очередь/длительныйnativejob могут привести кtimeout иотменеоперации, анепринудительнойостановке.
Проверки: test:system-database19pure/protocol tests иHTTP/JWT scripts passed (4background,4HTTPgate,4shutdown,4restore,3operation). Новый integration fixture среальнымbackup handler подтвердилHTTP202 иdrain, которыйне завершается доотложеннойcreate task. RIPE test сотключённымpersist иподменённымfetch подтвердил0fetch во времяpause ивозобновлениепослеresume. Существующийbackups-operations HTTP test passed;10certificate execution tests passed; contracts/backend build послеокончательныхизменений passed; diff-check passed. Тесты неочищалиБД инеприменялиRouterOS config.
Код пока неразвёрнут, основнойruntime673e5ba сохраняется; данные/CHR/auth стенданеизменялись. PG-6 остаётсяоткрытым доинтегрированногонаблюдения всехпутей. Restore ещёнепроверен; отдельнотребуется cache invalidation послевосстановления, иначеmemory topology/RIPE/settings могут отражатьпредыдущуюБД. Другиеэкземпляры/хосты registry негарантируетостанавливать. Следующий12.5.13 — integrated HTTP/scheduler/background/UDP quiescence ввыделеннойпроверочнойБД, аудитcache invalidation/deployment, затемPG-3/4. Локальныйкоммитбезpush.
## 12.5.11 — ожидание ручных API-запросов и закрытие live-подписок (2026-10-07)
Добавлен DatabaseRequestGate и root onRoute hook перед регистрацией API. Каждый API handler получает lease до выполнения и освобождает его вfinally после фактического завершения, а не onResponse/socket close. ВключеныGET: некоторые чтения имеют lazy/cache side effects. /health и /api/auth/config остаются доступными. Backup/restore захватывают exclusive gate на весь handler, включая audit event; новый API-запрос получает409 с понятной причиной, конкурирующая database operation409. Ожидание30сек; timeout отменяет команду, не объявляет незавершённые запросы завершёнными; при повторной попытке они остаются в active registry.
+13 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-07:12.5.11 завершён локально для HTTP handlers — backup/restore закрывают API admission, ждут завершения обработчиков и останавливают traffic live-подписки с ожиданиемcleanup. Четыре новых теста,15lifecycle/database tests,HTTP/JWT scripts иbuild passed. Основной runtime673e5ba не обновлён. Следующий12.5.12 — detached backup jobs/audit иRIPE persistence; PG-6/restore остаются открытыми.
Актуальное продолжение 2026-10-07:12.5.12 завершён локально — учтены detached backup jobs/audit, выпуск сертификатов, poll partition maintenance иbackground cache work; RIPE admission закрывается перед drain.19lifecycle/database tests,HTTP/JWT,backup/RIPE,10certificate tests иbuild passed. Основной runtime673e5ba не обновлён. Следующий12.5.13 — integrated HTTP/scheduler/background/UDP quiescence ввыделеннойБД, cache invalidation послеrestore иdeployment; PG-6/PG-3/4 ещёоткрыты.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -1054,3 +1054,15 @@ Traffic flow live и server traffic live регистрируют abort callback
Проверки:4новых gate tests passed — ожидание активного handler/отказ новых иконкурирующегоexclusive, timeout с нулёмdatabase commands иправильнымповторнымdrain, stream cancellation сожиданиемcleanup, Fastify child routes/GET409/health200/поздняярегистрацияabort/возобновлениеAPI. Полный test:system-database15pure/protocol tests иHTTP/JWT scripts passed; contracts/backend build passed; diff-check passed. Новые проверки включены вpackage script. Изменены index.ts, traffic.ts, traffic-flow.ts, добавлены gate service/plugin/test. RouterOS иданныестенда не менялись.
Следующий12.5.12 — зарегистрировать/дождаться detached backup jobs иaudit, закрыть admission идождатьсяRIPE resolver/persistence; послеэтого сборка/развёртывание ивнешняяPG-6/PG-3/4 проверка. Локальныйкоммит безpush.
## 12.5.12 — ожидание фоновых jobs, audit и записей RIPE (2026-10-07)
Добавлен общий process-local DatabaseBackgroundWork. Promise регистрируется до запуска отложенного тела; drain ждёт settlement всех задач, включая дочерние записи, которые регистрируются во время завершения parent task. Timeout30сек отменяет database operation; pending registry сохраняется для следующего drain. Rejection наблюдается отдельно и не ломает ожидание остальных задач; завершение drain не объявляет неуспешные записи успешными.
Backups HTTP202 теперь регистрирует job до microtask запуска и держит его учтённым до processBackupJob/terminal audit. Started/restore audit promises также учтены; ошибки audit не подменяют подтверждённую операцию. Выпуск сертификата изqueue учтён доprogress/terminal state/event. Опрос сервера учитывает detached dropExpiredPartitions/ensurePartitionsAround (DDL особенно опасен во времяrestore). Фоновые загрузки topology/catalog/интерфейсов/classification catalog также учтены, хотя часть изних только читаетБД/обновляетmemory cache.
RIPE учитывает loadPg, resolver worker иотдельные persist/persistAsn promises. Pause закрывает admission cache misses иочищает ещёневыданную best-effort enrichment очередь; текущие resolver иихSQL writes ожидаются. ПослеstopListener иscheduler drain database operation ждёт общий background registry; стандартный restart вновьразрешаетRIPE. Настоящий UDP worker передpool.end также вызываетpause/drain, поэтомуизолированные auxiliary writes не могут остаться заегоподтверждённымвыходом. Большая очередь/длительныйnativejob могут привести кtimeout иотменеоперации, анепринудительнойостановке.
Проверки: test:system-database19pure/protocol tests иHTTP/JWT scripts passed (4background,4HTTPgate,4shutdown,4restore,3operation). Новый integration fixture среальнымbackup handler подтвердилHTTP202 иdrain, которыйне завершается доотложеннойcreate task. RIPE test сотключённымpersist иподменённымfetch подтвердил0fetch во времяpause ивозобновлениепослеresume. Существующийbackups-operations HTTP test passed;10certificate execution tests passed; contracts/backend build послеокончательныхизменений passed; diff-check passed. Тесты неочищалиБД инеприменялиRouterOS config.
Код пока неразвёрнут, основнойruntime673e5ba сохраняется; данные/CHR/auth стенданеизменялись. PG-6 остаётсяоткрытым доинтегрированногонаблюдения всехпутей. Restore ещёнепроверен; отдельнотребуется cache invalidation послевосстановления, иначеmemory topology/RIPE/settings могут отражатьпредыдущуюБД. Другиеэкземпляры/хосты registry негарантируетостанавливать. Следующий12.5.13 — integrated HTTP/scheduler/background/UDP quiescence ввыделеннойпроверочнойБД, аудитcache invalidation/deployment, затемPG-3/4. Локальныйкоммитбезpush.
+4
View File
@@ -89,3 +89,7 @@ ssh -N -L 3000:127.0.0.1:3000 -L 8000:127.0.0.1:8000 [email protected]
## Локальный HTTP drain patch — 2026-10-07
12.5.11 добавляет instance-local gate для всех API handlers/GET, exclusive backup/restore иabort/drain traffic live streams.4gate tests,15lifecycle/database tests,HTTP/JWT scripts иbuild passed. Основнойruntime673e5ba остаётсяактивным. Detached backup jobs/audit иRIPE persistence ещёнеохвачены; следующий12.5.12 закрываетэтотпробел. PG-6/restore не приняты.
## Локальный detached work drain — 2026-10-07
12.5.12 добавляетучётbackground backup/audit/certificate/partition maintenance/cache work иpause RIPE enqueue сdrain текущихresolver/writes.19lifecycle/database tests,HTTP/JWT,backup/RIPE,10certificate tests иbuild прошли. На стенднеразвёрнуто; source673e5ba сохраняется. Следующий12.5.13 — integrated quiescence иcache invalidation передrestore/deployment. PG-6/3/4 остаютсяоткрытыми.