From 2943032462d31e97b773631bdd4dd4e791dc6380 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 7 Oct 2026 23:07:10 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D0=B4=D0=BE=D0=B6=D0=B4=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=D1=81=D1=8F=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B4=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B5=D0=B9=20=D1=81=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/src/routes/backups.ts | 15 ++--- .../services/certificate-issue-execution.ts | 3 +- .../services/database-background-work.test.ts | 62 +++++++++++++++++++ .../src/services/database-background-work.ts | 22 +++++++ backend/src/services/poller.ts | 3 +- .../src/services/system-database-backup.ts | 11 +++- backend/src/services/traffic-flow-classify.ts | 3 +- .../services/traffic-flow-collector-worker.ts | 2 + backend/src/services/traffic-flow-engine.ts | 5 +- backend/src/services/traffic-flow-ifaces.ts | 3 +- .../src/services/traffic-flow-ripe.test.ts | 16 ++++- backend/src/services/traffic-flow-ripe.ts | 29 ++++++--- docs/ux-ui-external-acceptance.md | 2 +- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 14 ++++- docs/ux-ui-stage-12.md | 14 ++++- docs/ux-ui-test-server.md | 4 ++ 18 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 backend/src/services/database-background-work.test.ts create mode 100644 backend/src/services/database-background-work.ts diff --git a/backend/package.json b/backend/package.json index d9414cce..beef4b63 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/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", diff --git a/backend/src/routes/backups.ts b/backend/src/routes/backups.ts index 76569eee..c9b8c7db 100644 --- a/backend/src/routes/backups.ts +++ b/backend/src/routes/backups.ts @@ -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 Promise { - 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 {}) return reply.status(202).send({ jobId, status: job.status, @@ -190,7 +191,7 @@ export function createBackupsRoutes(deps: { listServers?: () => Promise { 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 {}) return reply.send(result) } catch (err) { return reply.status(err instanceof BackupRestoreUnknownError ? 502 : 400).send({ diff --git a/backend/src/services/certificate-issue-execution.ts b/backend/src/services/certificate-issue-execution.ts index c56ecc02..1529926c 100644 --- a/backend/src/services/certificate-issue-execution.ts +++ b/backend/src/services/certificate-issue-execution.ts @@ -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 { void run(jobId).catch(report) }) + void databaseBackgroundWork.track(Promise.resolve().then(() => run(jobId))).catch(report) } return { run, queue } } diff --git a/backend/src/services/database-background-work.test.ts b/backend/src/services/database-background-work.test.ts new file mode 100644 index 00000000..5b77a395 --- /dev/null +++ b/backend/src/services/database-background-work.test.ts @@ -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(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() } +}) diff --git a/backend/src/services/database-background-work.ts b/backend/src/services/database-background-work.ts new file mode 100644 index 00000000..b1dc25a3 --- /dev/null +++ b/backend/src/services/database-background-work.ts @@ -0,0 +1,22 @@ +/** Includes child writes registered by tasks that are already running. */ +export class DatabaseBackgroundWork { + private pending = new Set>() + + track(task: Promise): Promise { + this.pending.add(task) + void task.then(() => this.pending.delete(task), () => this.pending.delete(task)) + return task + } + + async drain(timeoutMs = 30_000): Promise { + let timer: ReturnType | undefined + const timeout = new Promise((_, 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() diff --git a/backend/src/services/poller.ts b/backend/src/services/poller.ts index 8eabfa95..9d29f35b 100644 --- a/backend/src/services/poller.ts +++ b/backend/src/services/poller.ts @@ -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 { [serverId, inserted.polledAt], ) } - void dropExpiredPartitions(pool).then(() => ensurePartitionsAround(pool)) + void databaseBackgroundWork.track(dropExpiredPartitions(pool).then(() => ensurePartitionsAround(pool))).catch(() => {}) return toSnapshotRead(inserted!) } diff --git a/backend/src/services/system-database-backup.ts b/backend/src/services/system-database-backup.ts index 70c03a72..9c353bd9 100644 --- a/backend/src/services/system-database-backup.ts +++ b/backend/src/services/system-database-backup.ts @@ -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(fn: () => Promise): Promise { 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() + }, }), "Операция с базой приложения уже выполняется") } diff --git a/backend/src/services/traffic-flow-classify.ts b/backend/src/services/traffic-flow-classify.ts index 0a3e0367..0862963b 100644 --- a/backend/src/services/traffic-flow-classify.ts +++ b/backend/src/services/traffic-flow-classify.ts @@ -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 { /** Background refresh — analytics never awaits the HTTP. */ export function refreshFlowCatalogInBackground(): void { - void fetchCatalog() + void databaseBackgroundWork.track(fetchCatalog()) } diff --git a/backend/src/services/traffic-flow-collector-worker.ts b/backend/src/services/traffic-flow-collector-worker.ts index 4b7cdab6..4219802d 100644 --- a/backend/src/services/traffic-flow-collector-worker.ts +++ b/backend/src/services/traffic-flow-collector-worker.ts @@ -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 { 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() diff --git a/backend/src/services/traffic-flow-engine.ts b/backend/src/services/traffic-flow-engine.ts index 6da153e6..5cc07837 100644 --- a/backend/src/services/traffic-flow-engine.ts +++ b/backend/src/services/traffic-flow-engine.ts @@ -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) diff --git a/backend/src/services/traffic-flow-ifaces.ts b/backend/src/services/traffic-flow-ifaces.ts index 4e68614a..823afda9 100644 --- a/backend/src/services/traffic-flow-ifaces.ts +++ b/backend/src/services/traffic-flow-ifaces.ts @@ -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 } diff --git a/backend/src/services/traffic-flow-ripe.test.ts b/backend/src/services/traffic-flow-ripe.test.ts index 141e0438..744bf832 100644 --- a/backend/src/services/traffic-flow-ripe.test.ts +++ b/backend/src/services/traffic-flow-ripe.test.ts @@ -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)") diff --git a/backend/src/services/traffic-flow-ripe.ts b/backend/src/services/traffic-flow-ripe.ts index 4c5e752b..fdc629a2 100644 --- a/backend/src/services/traffic-flow-ripe.ts +++ b/backend/src/services/traffic-flow-ripe.ts @@ -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() const inflight = new Map>() const queue: string[] = [] const queued = new Set() +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 { @@ -204,7 +206,7 @@ async function loadPg(): Promise { 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 { } } 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): 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): void { queued.add(ip) queue.push(ip) } - if (queue.length) void runWorker() + if (queue.length) void databaseBackgroundWork.track(runWorker()) +} + +export async function pauseRipeBackgroundWork(): Promise { + databasePaused = true + queue.length = 0 + queued.clear() + await databaseBackgroundWork.drain() +} + +export function resumeRipeBackgroundWork(): void { + databasePaused = false } diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index a391e948..55e9d42c 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. Блок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 достаточно следующих запросов чтения для начальной проверки, без вывода секретов: diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 499343ff..5df7a815 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 6297f80a..cf504640 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -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. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index adec1d14..ca49fbc4 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.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. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 8de9871e..694848c0 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -89,3 +89,7 @@ ssh -N -L 3000:127.0.0.1:3000 -L 8000:127.0.0.1:8000 root@161.104.46.222 ## Локальный 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 остаютсяоткрытыми.