fix(api): дождаться заданий планировщика перед операцией с БД
This commit is contained in:
1 parent
6720ad3539
commit
1b89d56ea3
13 files changed
+175
-12
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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<Parameters<typeof createSchedulerRoutes>[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() }
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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<void> {
|
||||
const run = alertEngineRunner
|
||||
if (!run) return
|
||||
while (isSchedulerJobRunning("alert_engine")) {
|
||||
if (isSchedulerJobsPaused()) return
|
||||
await new Promise<void>((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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -3,6 +3,33 @@
|
||||
* Слот берётся до `await` в теле джобы — так `alert_engine` может дождаться REST и др. до чтения БД.
|
||||
*/
|
||||
const runningSchedulerJobs = new Set<string>()
|
||||
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<void> {
|
||||
paused = true
|
||||
if (!runningSchedulerJobs.size) return
|
||||
await new Promise<void>((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<string>([
|
||||
"traffic",
|
||||
@@ -15,13 +42,14 @@ const ALERT_SNAPSHOT_SOURCE_JOB_KEYS = new Set<string>([
|
||||
|
||||
/** 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 {
|
||||
|
||||
@@ -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<string, ReturnType<typeof setInterval>>()
|
||||
let schedulerGeneration = 0
|
||||
|
||||
const RUN_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
@@ -203,8 +207,9 @@ export async function runSchedulerJobNow(jobKey: string): Promise<SchedulerRunRe
|
||||
throw new Error(`Unknown job key: ${jobKey}`)
|
||||
}
|
||||
if (!tryBeginSchedulerJob(jobKey)) {
|
||||
const err = new Error("Job already running")
|
||||
const err = new Error(isSchedulerJobsPaused() ? "Планировщик приостановлен для операции с базой" : "Job already running")
|
||||
;(err as Error & { statusCode?: number }).statusCode = 409
|
||||
if (isSchedulerJobsPaused()) (err as Error & { code?: string }).code = "SCHEDULER_PAUSED"
|
||||
throw err
|
||||
}
|
||||
try {
|
||||
@@ -222,9 +227,13 @@ function clearAllTimers() {
|
||||
|
||||
/** Пересоздать интервалы после смены настроек. */
|
||||
export async function refreshScheduler(): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
)
|
||||
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
}
|
||||
|
||||
export function stopScheduler(): void {
|
||||
schedulerGeneration += 1
|
||||
clearAllTimers()
|
||||
}
|
||||
|
||||
export async function stopSchedulerForDatabaseOperation(): Promise<void> {
|
||||
const drained = pauseSchedulerJobsAndWait()
|
||||
stopScheduler()
|
||||
await drained
|
||||
clearAlertCollectorHooksTimer()
|
||||
}
|
||||
|
||||
export async function restartSchedulerAfterDatabaseOperation(): Promise<void> {
|
||||
resumeSchedulerJobs()
|
||||
await refreshScheduler()
|
||||
}
|
||||
|
||||
export function isJobRunning(jobKey: string): boolean {
|
||||
return isSchedulerJobRunning(jobKey)
|
||||
}
|
||||
|
||||
@@ -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<T extends object>(fn: () => Promise<T>): Promise<T & DatabaseOperationWarnings> {
|
||||
return withOperationLock(() => pool.connect(), BACKUP_LOCK_KEY, () => executeDatabaseOperation(fn, {
|
||||
stopListener: stopTrafficFlowListener,
|
||||
stopScheduler,
|
||||
stopScheduler: stopSchedulerForDatabaseOperation,
|
||||
startListener: startTrafficFlowListener,
|
||||
startScheduler: refreshScheduler,
|
||||
startScheduler: restartSchedulerAfterDatabaseOperation,
|
||||
}), "Операция с базой приложения уже выполняется")
|
||||
}
|
||||
|
||||
|
||||
@@ -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 достаточно следующих запросов чтения для начальной проверки, без вывода секретов:
|
||||
|
||||
|
||||
+1
-1
@@ -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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
|
||||
+11
-1
@@ -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.
|
||||
|
||||
+11
-1
@@ -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.
|
||||
@@ -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. Восстановление основной тестовой БД ещё не выполнялось.
|
||||
Reference in new issue
Block a user