fix(api): дождаться фоновых записей перед операцией с БД
This commit is contained in:
1 parent
8682f2a7a5
commit
2943032462
18 files changed
+183
-29
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/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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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 остаютсяоткрытыми.
|
||||
Reference in new issue
Block a user