From 1b89d56ea3d20c2b23acff2db70538b816fc9b70 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 7 Oct 2026 22:41:50 +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=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D1=89?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20=D0=BE?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D1=81=20?= =?UTF-8?q?=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/scheduler.test.ts | 12 ++++ backend/src/routes/scheduler.ts | 3 +- backend/src/services/alert-collector-hooks.ts | 5 +- .../src/services/scheduler-running.test.ts | 67 +++++++++++++++++++ backend/src/services/scheduler-running.ts | 30 ++++++++- backend/src/services/scheduler.ts | 30 ++++++++- .../src/services/system-database-backup.ts | 6 +- docs/ux-ui-external-acceptance.md | 2 +- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 12 +++- docs/ux-ui-stage-12.md | 12 +++- docs/ux-ui-test-server.md | 4 ++ 13 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 backend/src/services/scheduler-running.test.ts diff --git a/backend/package.json b/backend/package.json index 8baee8d2..ef6f7cb3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,7 @@ "test:recursive-routes": "tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts", "test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts", "test:ros-file": "tsx src/services/ros-file-contents.test.ts", - "test:scheduler": "tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts", + "test:scheduler": "tsx src/services/scheduler-running.test.ts && tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts", "test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts", "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts", "test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database", diff --git a/backend/src/routes/scheduler.test.ts b/backend/src/routes/scheduler.test.ts index 77700458..b86497a0 100644 --- a/backend/src/routes/scheduler.test.ts +++ b/backend/src/routes/scheduler.test.ts @@ -3,12 +3,24 @@ import { test } from "node:test" import Fastify from "fastify" import { createSchedulerRoutes } from "./scheduler.js" import type { SchedulerRunReceipt } from "../services/scheduler-recorded-run.js" +import { pauseSchedulerJobsAndWait, resumeSchedulerJobs } from "../services/scheduler-running.js" +import { runSchedulerJobNow } from "../services/scheduler.js" const receipt: SchedulerRunReceipt = { status: "ok", error: null, result: { v: 1, job: "backups", sampledAt: "2026-10-01T00:00:00Z", due: true, created: 1, failures: 0, pruned: 0 } } async function setup(patch: Partial[0]> = {}) { const app = Fastify(); let calls = 0 await app.register(createSchedulerRoutes({ status: async () => ({ jobs: [] }), runs: async () => ({ runs: [] }), refresh: async () => {}, run: async () => { calls++; return receipt }, ...patch })) return { app, calls: () => calls } } + +test("database pause denies the real manual runner with an accurate HTTP conflict", async () => { + await pauseSchedulerJobsAndWait() + const { app } = await setup({ run: runSchedulerJobNow }) + try { + const response = await app.inject({ method: "POST", url: "/scheduler/jobs/backups/run-now" }) + assert.equal(response.statusCode, 409) + assert.equal(response.json().error, "Планировщик приостановлен для операции с базой") + } finally { resumeSchedulerJobs(); await app.close() } +}) test("manual partial receipt is not ok and retains confirmed backup result", async () => { const { app } = await setup({ run: async () => ({ ...receipt, status: "error", error: "S3 unavailable" }) }) try { const response = await app.inject({ method: "POST", url: "/scheduler/jobs/backups/run-now" }); assert.equal(response.statusCode, 200); assert.equal(response.json().ok, false); assert.equal(response.json().result.result.created, 1); assert.equal(response.json().result.error, "S3 unavailable") } finally { await app.close() } diff --git a/backend/src/routes/scheduler.ts b/backend/src/routes/scheduler.ts index b8372594..d04bfae5 100644 --- a/backend/src/routes/scheduler.ts +++ b/backend/src/routes/scheduler.ts @@ -52,7 +52,8 @@ return async (app) => { } catch (e) { const statusCode = (e as Error & { statusCode?: number }).statusCode if (statusCode === 409) { - return reply.status(409).send({ error: "Задача уже выполняется" }) + const paused = (e as Error & { code?: string }).code === "SCHEDULER_PAUSED" + return reply.status(409).send({ error: paused ? "Планировщик приостановлен для операции с базой" : "Задача уже выполняется" }) } const msg = e instanceof Error ? e.message : String(e) return reply.status(500).send({ error: msg }) diff --git a/backend/src/services/alert-collector-hooks.ts b/backend/src/services/alert-collector-hooks.ts index 381fa457..1821d751 100644 --- a/backend/src/services/alert-collector-hooks.ts +++ b/backend/src/services/alert-collector-hooks.ts @@ -5,7 +5,7 @@ * Любой код, пишущий таблицы для `buildSignalSnapshot`, после успешного коммита должен вызывать * `scheduleAlertEngineAfterDataCollectors()`. */ -import { isSchedulerJobRunning } from "./scheduler-running.js" +import { isSchedulerJobRunning, isSchedulerJobsPaused } from "./scheduler-running.js" const ALERT_AFTER_COLLECT_DEBOUNCE_MS = 250 const ALERT_AFTER_COLLECT_WAIT_MS = 100 @@ -31,13 +31,16 @@ async function runAlertEngineWhenIdle(): Promise { const run = alertEngineRunner if (!run) return while (isSchedulerJobRunning("alert_engine")) { + if (isSchedulerJobsPaused()) return await new Promise((resolve) => setTimeout(resolve, ALERT_AFTER_COLLECT_WAIT_MS)) } + if (isSchedulerJobsPaused()) return await run() } /** После успешной записи сигналов в БД коллекторами (не из `alert_engine`). */ export function scheduleAlertEngineAfterDataCollectors(): void { + if (isSchedulerJobsPaused()) return if (alertAfterCollectTimer != null) clearTimeout(alertAfterCollectTimer) alertAfterCollectTimer = setTimeout(async () => { alertAfterCollectTimer = null diff --git a/backend/src/services/scheduler-running.test.ts b/backend/src/services/scheduler-running.test.ts new file mode 100644 index 00000000..41f0d13c --- /dev/null +++ b/backend/src/services/scheduler-running.test.ts @@ -0,0 +1,67 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { + endSchedulerJob, isSchedulerJobsPaused, isSchedulerJobRunning, + pauseSchedulerJobsAndWait, resumeSchedulerJobs, tryBeginSchedulerJob, +} from "./scheduler-running.js" +import { + clearAlertCollectorHooksTimer, scheduleAlertEngineAfterDataCollectors, wireAlertEngineRunner, +} from "./alert-collector-hooks.js" +import { executeDatabaseOperation } from "./system-database-operation.js" + +test("database preparation waits for all jobs and denies new admission", async () => { + assert.equal(tryBeginSchedulerJob("traffic"), true) + assert.equal(tryBeginSchedulerJob("uptime_speed"), true) + let completed = false + const drained = pauseSchedulerJobsAndWait().then(() => { completed = true }) + try { + assert.equal(isSchedulerJobsPaused(), true) + assert.equal(tryBeginSchedulerJob("manual"), false) + assert.throws(resumeSchedulerJobs, /ожидает/) + endSchedulerJob("traffic") + await Promise.resolve() + assert.equal(completed, false) + assert.equal(isSchedulerJobRunning("uptime_speed"), true) + endSchedulerJob("uptime_speed") + await drained + assert.equal(completed, true) + assert.equal(tryBeginSchedulerJob("manual"), false) + } finally { + endSchedulerJob("traffic"); endSchedulerJob("uptime_speed") + await drained; resumeSchedulerJobs() + } + assert.equal(tryBeginSchedulerJob("manual"), true) + endSchedulerJob("manual") +}) + +test("drain timeout prevents database command and recovery reopens admission", async () => { + assert.equal(tryBeginSchedulerJob("slow"), true) + let commands = 0, restarts = 0 + try { + await assert.rejects(executeDatabaseOperation(async () => { commands++; return {} }, { + stopListener: () => {}, stopScheduler: () => pauseSchedulerJobsAndWait(10), + startListener: () => { restarts++ }, startScheduler: resumeSchedulerJobs, + }), /операция с базой отменена/) + assert.equal(commands, 0) + assert.equal(restarts, 1) + assert.equal(isSchedulerJobsPaused(), false) + assert.equal(isSchedulerJobRunning("slow"), true) + assert.equal(tryBeginSchedulerJob("slow"), false) + } finally { endSchedulerJob("slow"); resumeSchedulerJobs() } +}) + +test("a queued alert hook and collectors finishing during pause cannot start a job", async () => { + let runs = 0 + wireAlertEngineRunner(async () => { runs++ }) + scheduleAlertEngineAfterDataCollectors() + await pauseSchedulerJobsAndWait() + try { + scheduleAlertEngineAfterDataCollectors() + await new Promise(resolve => setTimeout(resolve, 300)) + assert.equal(runs, 0) + } finally { clearAlertCollectorHooksTimer(); resumeSchedulerJobs() } + scheduleAlertEngineAfterDataCollectors() + await new Promise(resolve => setTimeout(resolve, 300)) + assert.equal(runs, 1) + clearAlertCollectorHooksTimer() +}) diff --git a/backend/src/services/scheduler-running.ts b/backend/src/services/scheduler-running.ts index 9aa5f77b..ccc92e0a 100644 --- a/backend/src/services/scheduler-running.ts +++ b/backend/src/services/scheduler-running.ts @@ -3,6 +3,33 @@ * Слот берётся до `await` в теле джобы — так `alert_engine` может дождаться REST и др. до чтения БД. */ const runningSchedulerJobs = new Set() +let paused = false +const idleWaiters = new Set<() => void>() + +export function isSchedulerJobsPaused(): boolean { + return paused +} + +/** Close admission before waiting: completion includes the job's database journal. */ +export async function pauseSchedulerJobsAndWait(timeoutMs = 30_000): Promise { + paused = true + if (!runningSchedulerJobs.size) return + await new Promise((resolve, reject) => { + const complete = () => { clearTimeout(timer); idleWaiters.delete(complete); resolve() } + const timer = setTimeout(() => { + idleWaiters.delete(complete) + reject(new Error("Не удалось дождаться завершения заданий планировщика; операция с базой отменена")) + }, timeoutMs) + idleWaiters.add(complete) + }) +} + +export function resumeSchedulerJobs(): void { + if (runningSchedulerJobs.size && idleWaiters.size) { + throw new Error("Планировщик ещё ожидает завершения заданий") + } + paused = false +} const ALERT_SNAPSHOT_SOURCE_JOB_KEYS = new Set([ "traffic", @@ -15,13 +42,14 @@ const ALERT_SNAPSHOT_SOURCE_JOB_KEYS = new Set([ /** true, если слот занят (джоба уже выполняется). */ export function tryBeginSchedulerJob(jobKey: string): boolean { - if (runningSchedulerJobs.has(jobKey)) return false + if (paused || runningSchedulerJobs.has(jobKey)) return false runningSchedulerJobs.add(jobKey) return true } export function endSchedulerJob(jobKey: string): void { runningSchedulerJobs.delete(jobKey) + if (!runningSchedulerJobs.size) for (const complete of [...idleWaiters]) complete() } export function isSchedulerJobRunning(jobKey: string): boolean { diff --git a/backend/src/services/scheduler.ts b/backend/src/services/scheduler.ts index b46c8863..8ac44127 100644 --- a/backend/src/services/scheduler.ts +++ b/backend/src/services/scheduler.ts @@ -52,6 +52,9 @@ import { endSchedulerJob, isSchedulerJobRunning, tryBeginSchedulerJob, + isSchedulerJobsPaused, + pauseSchedulerJobsAndWait, + resumeSchedulerJobs, } from "./scheduler-running.js" import { appendEvent } from "../modules/events/service/events-service.js" import { runRecordedSchedulerJob, type SchedulerRunReceipt } from "./scheduler-recorded-run.js" @@ -72,6 +75,7 @@ export const JOB_KEYS = [ export type SchedulerJobKey = (typeof JOB_KEYS)[number] const timers = new Map>() +let schedulerGeneration = 0 const RUN_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1000 @@ -203,8 +207,9 @@ export async function runSchedulerJobNow(jobKey: string): Promise { + if (isSchedulerJobsPaused()) return + const generation = ++schedulerGeneration + const superseded = () => generation !== schedulerGeneration || isSchedulerJobsPaused() clearAllTimers() const traffic = await getTrafficSettings() + if (superseded()) return if (traffic.enabled) { const ms = Math.max(5_000, traffic.intervalSec * 1000) void executeSchedulerJob("traffic").catch(() => {}) @@ -237,7 +246,9 @@ export async function refreshScheduler(): Promise { } const apiPing = await getServersApiPingSettings() + if (superseded()) return const internetPath = await getInternetPathSettings() + if (superseded()) return if (apiPing.enabled) { const apiMs = Math.max(10_000, apiPing.intervalSec * 1000) void executeSchedulerJob("servers_rest_ping").catch(() => {}) @@ -250,6 +261,7 @@ export async function refreshScheduler(): Promise { } const uptime = await getUptimeSettings() + if (superseded()) return const resOn = uptime.resourcesEnabled ?? uptime.enabled const pingOn = uptime.pingEnabled ?? uptime.enabled const spdOn = uptime.speedEnabled ?? uptime.enabled @@ -308,6 +320,7 @@ export async function refreshScheduler(): Promise { ) const certRenew = await getCertificateRenewSettings() + if (superseded()) return if (certRenew.enabled) { const certRenewMs = Math.max(300_000, certRenew.intervalSec * 1000) void executeSchedulerJob("certificates_renew").catch(() => {}) @@ -320,6 +333,7 @@ export async function refreshScheduler(): Promise { } const backupSchedule = await getBackupScheduleSettings() + if (superseded()) return if (backupSchedule.enabled) { const backupMs = 60_000 void executeSchedulerJob("backups").catch(() => {}) @@ -332,6 +346,7 @@ export async function refreshScheduler(): Promise { } const geoip = await getGeoipSettings() + if (superseded()) return if (geoip.enabled) { const geoipMs = Math.max(6 * 3600_000, geoip.updateIntervalSec * 1000) void executeSchedulerJob("geoip_update").catch(() => {}) @@ -354,9 +369,22 @@ export async function refreshScheduler(): Promise { } export function stopScheduler(): void { + schedulerGeneration += 1 clearAllTimers() } +export async function stopSchedulerForDatabaseOperation(): Promise { + const drained = pauseSchedulerJobsAndWait() + stopScheduler() + await drained + clearAlertCollectorHooksTimer() +} + +export async function restartSchedulerAfterDatabaseOperation(): Promise { + resumeSchedulerJobs() + await refreshScheduler() +} + export function isJobRunning(jobKey: string): boolean { return isSchedulerJobRunning(jobKey) } diff --git a/backend/src/services/system-database-backup.ts b/backend/src/services/system-database-backup.ts index 21591c3c..42b853bf 100644 --- a/backend/src/services/system-database-backup.ts +++ b/backend/src/services/system-database-backup.ts @@ -8,7 +8,7 @@ import { env } from "../config.js" import { pool } from "../db/index.js" import { withOperationLock } from "../db/operation-lock.js" import { executeDatabaseOperation, type DatabaseOperationWarnings } from "./system-database-operation.js" -import { refreshScheduler, stopScheduler } from "./scheduler.js" +import { restartSchedulerAfterDatabaseOperation, stopSchedulerForDatabaseOperation } from "./scheduler.js" import { startTrafficFlowListener, stopTrafficFlowListener, @@ -49,9 +49,9 @@ 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: stopTrafficFlowListener, - stopScheduler, + stopScheduler: stopSchedulerForDatabaseOperation, startListener: startTrafficFlowListener, - startScheduler: refreshScheduler, + startScheduler: restartSchedulerAfterDatabaseOperation, }), "Операция с базой приложения уже выполняется") } diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index eb93f42f..bd9c0451 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 до его начала. -Остановка таймеров scheduler и локальный вызов stopTrafficFlowListener сами по себе не доказывают завершение всех выполняющихся задач/flush или остановку экземпляров на других хостах. PG-6 остаётся открытым до наблюдаемого подтверждения; восстановление стенда выполняется после его получения. Реальные тесты backend, способные очищать таблицы, запускаются только против выделенной тестовой БД. +В локальном блоке12.5.8 добавлено ожидание текущих scheduler jobs с закрытием admission, но этот код ещё не развёрнут на стенде673e5ba. Локальный вызов stopTrafficFlowListener пока не подтверждает завершение worker/flush; ручные API writers и экземпляры на других хостах также требуют отдельного контроля. PG-6 остаётся открытым до наблюдаемого подтверждения; восстановление стенда выполняется после его получения. Реальные тесты backend, способные очищать таблицы, запускаются только против выделенной тестовой БД. После bootstrap достаточно следующих запросов чтения для начальной проверки, без вывода секретов: diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 60ddc894..2985aba4 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-07: блок 12.5.7 завершён. Реальные PG-2 (backup и проверочное восстановление отдельной БД) и PG-5 (backup/restore конфликт 409) проверены; PG-7 подтверждён в границах HTTP RBAC. Следующий 12.5.8 — завершение текущих jobs/flush перед restore и проверка PG-6. Вся 12.5 остаётся открытой. +Актуальное продолжение 2026-10-07: блок 12.5.8 завершён в части scheduler lifecycle. Операции с БД закрывают новые запуски и ждут текущие jobs; таймаут отменяет команду, ручной запуск сообщает точный409. Локальные профильные проверки прошли. Изменения ещё не развёрнуты на стенде673e5ba; PG-6 остаётся открытым. Следующий 12.5.9 — graceful stop/flush NetFlow worker и проверка lifecycle перед restore. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-07. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 47432991..ab661cb5 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-07: блок 12.5.7 завершён. Реальные PG-2 (backup и проверочное восстановление отдельной БД) и PG-5 (backup/restore конфликт 409) проверены; PG-7 подтверждён в границах HTTP RBAC. Следующий 12.5.8 — завершение текущих jobs/flush перед restore и проверка PG-6. Вся 12.5 остаётся открытой. +Актуальное продолжение 2026-10-07: блок 12.5.8 завершён в части scheduler lifecycle. Операции с БД закрывают новые запуски и ждут текущие jobs; таймаут отменяет команду, ручной запуск сообщает точный409. Локальные профильные проверки прошли. Изменения ещё не развёрнуты на стенде673e5ba; PG-6 остаётся открытым. Следующий 12.5.9 — graceful stop/flush NetFlow worker и проверка lifecycle перед restore. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -34,6 +34,16 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в описанных границах:12.3.83,main1186/1186,context221/221. Этап остаётся в работе: стенд12.5.1–3 развёрнут,bootstrap/smoke проверены;остальные сценарии12.5 не выполнены. | +## 12.5.8 — ожидание заданий планировщика перед операцией с БД (2026-10-07) + +Исправлена первая часть обнаруженного в12.5.7 lifecycle: scheduler-running закрывает admission до ожидания всех активных jobs. Слот освобождается в существующем finally после collector и записи scheduler journal. Ожидание ограничено30сек; при таймауте pg_dump/pg_restore не запускается, исходная ошибка сохраняется, consumers проходят стандартную попытку перезапуска. Повторный запуск уже выполняющейся job по-прежнему отклонён. system-database-backup подключён к новым stopSchedulerForDatabaseOperation/restartSchedulerAfterDatabaseOperation; обычный stopScheduler для существующих потребителей сохраняет синхронный контракт. + +Новые ручные запуски во время паузы получают409 «Планировщик приостановлен для операции с базой», вместо неверного «Задача уже выполняется». Alert collector hooks не планируют/не запускают новые jobs во время паузы; уже ожидающий hook проверяет паузу перед runner. RefreshScheduler защищён поколением: stop или более новый refresh отменяет продолжение старого после каждого await чтения настроек. Refresh в период database pause не создаёт интервалы; штатный restart прочитает актуальные настройки. + +Изменены scheduler-running.ts, scheduler.ts, alert-collector-hooks.ts, system-database-backup.ts и scheduler route; новый lifecycle test включён в test:scheduler. Проверки: test:scheduler17/17 (3lifecycle,9recorded-run,5HTTP), test:system-database (7pure tests и HTTP/JWT scripts) passed; contracts/backend build passed. Проверены два одновременно активных jobs/отказ нового запуска, сохранение паузы до завершения, timeout с нулём вызовов database command, recovery, queued alert hook и HTTP409 с настоящим manual runner. Эти проверки не требуют native RouterOS apply или очистки тестовой БД. + +Это локальный кодовый блок, не завершение PG-6. NetFlow ещё использует fire-and-forget flush/terminate; прочие ручные API writers и другие экземпляры не охвачены scheduler gate. Активный Docker source остаётся673e5ba, новый код пока не развёрнут. Восстановление основной тестовой БД не запускалось; данные/CHR/серверный auth режим не менялись. Следующий 12.5.9 — graceful stop с подтверждением завершения worker flush и выхода worker, затем сборка/развёртывание и наблюдаемая PG-6 приёмка. Локальный коммит без push. + ## 12.5.7 — реальный архив PostgreSQL, права и конкуренция операций (2026-10-07) Стенд 161.104.46.222, source 673e5ba, PostgreSQL/server и pg_restore/client 18.6. [Очищенный отчёт](ux-ui/acceptance/pg-backup-auth-2026-10-07.json). Для проверки прав временно включён AUTH_REQUIRED=true; JWT с тестовым issuer сформированы только в памяти контейнера. Восемь запросов backup/restore: anonymous и wrong issuer → 401, settings writer и wrong app → 403. Admin mm:settings:admin получил backup HTTP200, 475225 байт, PGDMP, SHA256 a6b66402b28bdb146cae9712c384979aa1c1000fee19cbc97c0ec045b673c002, outcome {} без предупреждений. Настоящий Portal SSO и отсутствие процессов при отказах отдельно не инструментированы: PG-7 принят только в границах HTTP RBAC. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 988fc33a..2e19e9eb 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.7 завершён. Реальные PG-2 (backup и проверочное восстановление отдельной БД) и PG-5 (backup/restore конфликт 409) проверены; PG-7 подтверждён в границах HTTP RBAC. Следующий 12.5.8 — завершение текущих jobs/flush перед restore и проверка PG-6. Вся 12.5 остаётся открытой. +Актуальное продолжение 2026-10-07: блок 12.5.8 завершён в части scheduler lifecycle. Операции с БД закрывают новые запуски и ждут текущие jobs; таймаут отменяет команду, ручной запуск сообщает точный409. Локальные профильные проверки прошли. Изменения ещё не развёрнуты на стенде673e5ba; PG-6 остаётся открытым. Следующий 12.5.9 — graceful stop/flush NetFlow worker и проверка lifecycle перед restore. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -1010,3 +1010,13 @@ PG-2: pg_restore --list прочитал 907 TOC entries, включая servers PG-5: отдельная SQL-транзакция временно удерживала ACCESS EXCLUSIVE lock таблицы servers, первый HTTP backup запустил реальный pg_dump. Независимое соединение наблюдателя подтвердило application_name=pg_dump/wait_event_type=Lock/wait_event=relation. Конкурирующий POST restore с валидным доверенным архивом получил409 «Операция с базой приложения уже выполняется». После ROLLBACK тестового lock первый backup вернул200/PGDMP/478793 байта. Первая попытка наблюдения использовала кешированный stats snapshot внутри удерживающей транзакции и не увидела процесс; исправлена отдельным observer connection, повторная проверка прошла. Первый контрольный SQL ошибочно называл таблицу uptime_speed_runs; исправлен на uptime_speed_test_runs. Выявлен незакрытый PG-6: stopScheduler отменяет таймеры без ожидания текущих jobs; stopTrafficFlowListener вызывает flushPending без await и не подтверждает завершение worker. API restore основной БД не запускался. После проверки AUTH_REQUIRED возвращён в false, /api/auth/config это подтвердил, /health ok/version1.0.0-test.673e5ba; listener по-прежнему disabled/alivefalse/boundfalse. Код продукта и RouterOS в этом блоке не менялись. Следующий 12.5.8: обеспечить ожидание завершения jobs/flush/worker, проверить lifecycle перед PG-3/4; остальной протокол остаётся открытым. Локальный коммит без push. + +## 12.5.8 — ожидание заданий планировщика перед операцией с БД (2026-10-07) + +Исправлена первая часть обнаруженного в12.5.7 lifecycle: scheduler-running закрывает admission до ожидания всех активных jobs. Слот освобождается в существующем finally после collector и записи scheduler journal. Ожидание ограничено30сек; при таймауте pg_dump/pg_restore не запускается, исходная ошибка сохраняется, consumers проходят стандартную попытку перезапуска. Повторный запуск уже выполняющейся job по-прежнему отклонён. system-database-backup подключён к новым stopSchedulerForDatabaseOperation/restartSchedulerAfterDatabaseOperation; обычный stopScheduler для существующих потребителей сохраняет синхронный контракт. + +Новые ручные запуски во время паузы получают409 «Планировщик приостановлен для операции с базой», вместо неверного «Задача уже выполняется». Alert collector hooks не планируют/не запускают новые jobs во время паузы; уже ожидающий hook проверяет паузу перед runner. RefreshScheduler защищён поколением: stop или более новый refresh отменяет продолжение старого после каждого await чтения настроек. Refresh в период database pause не создаёт интервалы; штатный restart прочитает актуальные настройки. + +Изменены scheduler-running.ts, scheduler.ts, alert-collector-hooks.ts, system-database-backup.ts и scheduler route; новый lifecycle test включён в test:scheduler. Проверки: test:scheduler17/17 (3lifecycle,9recorded-run,5HTTP), test:system-database (7pure tests и HTTP/JWT scripts) passed; contracts/backend build passed. Проверены два одновременно активных jobs/отказ нового запуска, сохранение паузы до завершения, timeout с нулём вызовов database command, recovery, queued alert hook и HTTP409 с настоящим manual runner. Эти проверки не требуют native RouterOS apply или очистки тестовой БД. + +Это локальный кодовый блок, не завершение PG-6. NetFlow ещё использует fire-and-forget flush/terminate; прочие ручные API writers и другие экземпляры не охвачены scheduler gate. Активный Docker source остаётся673e5ba, новый код пока не развёрнут. Восстановление основной тестовой БД не запускалось; данные/CHR/серверный auth режим не менялись. Следующий 12.5.9 — graceful stop с подтверждением завершения worker flush и выхода worker, затем сборка/развёртывание и наблюдаемая PG-6 приёмка. Локальный коммит без push. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 9ddff43f..1356fcd1 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -73,3 +73,7 @@ ssh -N -L 3000:127.0.0.1:3000 -L 8000:127.0.0.1:8000 root@161.104.46.222 [Отчёт 12.5.7](ux-ui/acceptance/pg-backup-auth-2026-10-07.json): admin backup200/475225bytes, pg_restore18.6 прочитал907TOC entries и восстановил отдельную mmapp_acceptance_20261007. Контрольные записи/9миграций/5speed rows/servers sequence совпали с исходной БД. Приложение не подключено к проверочной БД; она сохранена для дальнейшей приёмки. Дамп только в /opt/mmapp-test/data/acceptance-private (0700), файл0600, содержит приватные учётные данные и не копируется в Git. При AUTH_REQUIRED=true подтверждены401/403 на backup/restore; JWT сформированы в памяти, реальный Portal SSO не проверен. Конкурирующий restore409 при наблюдаемом активном pg_dump, первый backup200 после снятия контрольного SQL lock. AUTH_REQUIRED=false восстановлен, API auth/config и health подтвердили состояние. PG-3/4/6 остаются открытыми: ожидание завершения текущих scheduler jobs/flush/worker пока не обеспечено. + +## Локальный lifecycle patch — 2026-10-07 + +Блок12.5.8 исправляет ожидание scheduler jobs и запрещает новые запуски во время database operation;17scheduler tests,7pure database tests,HTTP/JWT scripts и build прошли. На этом стенде patch пока не развёрнут: активный source673e5ba сохраняется. Следующий12.5.9 — graceful NetFlow worker stop/flush; только после этого перейти к сборке/развёртыванию и внешней проверкеPG-6. Восстановление основной тестовой БД ещё не выполнялось.