From b2cf71fcc7f786565dd8cb5d56bf5647f091fd13 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 15 Oct 2026 22:04:38 +0700 Subject: [PATCH] =?UTF-8?q?fix(db):=20=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BA=D0=B8=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D1=89=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0013_scheduler_retention_index.sql | 4 ++ backend/package.json | 2 +- .../src/db/scheduler-retention-index.test.ts | 33 ++++++++++++ backend/src/db/schema.ts | 1 + deploy/PRODUCTION-RETENTION-INDEX.md | 50 +++++++++++++++++++ docs/ux-ui-progress.md | 8 ++- docs/ux-ui/acceptance/prod266-retention.json | 40 +++++++++++++++ 7 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 backend/drizzle/0013_scheduler_retention_index.sql create mode 100644 backend/src/db/scheduler-retention-index.test.ts create mode 100644 deploy/PRODUCTION-RETENTION-INDEX.md create mode 100644 docs/ux-ui/acceptance/prod266-retention.json diff --git a/backend/drizzle/0013_scheduler_retention_index.sql b/backend/drizzle/0013_scheduler_retention_index.sql new file mode 100644 index 00000000..ec1ef14d --- /dev/null +++ b/backend/drizzle/0013_scheduler_retention_index.sql @@ -0,0 +1,4 @@ +-- Retention runs after each scheduler receipt; avoid scanning the complete history. +-- On a busy existing installation, pre-create this same index CONCURRENTLY +-- outside a transaction before deploying (deploy/PRODUCTION-RETENTION-INDEX.md). +CREATE INDEX IF NOT EXISTS idx_scheduler_runs_finished_at ON scheduler_runs (finished_at); diff --git a/backend/package.json b/backend/package.json index 4217a6d3..041f5d85 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,7 @@ "test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts", "test:traffic-flow": "tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts", "test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts", - "test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts", + "test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/db/scheduler-retention-index.test.ts && tsx src/services/config-revisions.test.ts", "test:recursive-routes": "tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-apply.test.ts", "test:config-sync": "tsx src/utils/server-id.test.ts && tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts && tsx src/services/gre-native-identity.test.ts && tsx src/services/gre-ipv6.test.ts && tsx src/routes/gre-ipv6-validation.test.ts && tsx src/services/network-disabled-server.test.ts", "test:ros-file": "tsx src/services/ros-file-contents.test.ts && tsx src/services/mikrotik-response.test.ts", diff --git a/backend/src/db/scheduler-retention-index.test.ts b/backend/src/db/scheduler-retention-index.test.ts new file mode 100644 index 00000000..d33fd957 --- /dev/null +++ b/backend/src/db/scheduler-retention-index.test.ts @@ -0,0 +1,33 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" +import pg from "pg" +import { env } from "../config.js" + +// No bootstrap/migrations against public tables: this test needs only a connection. +const pool = new pg.Pool({ connectionString: env.DATABASE_URL, connectionTimeoutMillis: 8000, statement_timeout: 10000 }) +try { + const client = await pool.connect() + try { + await client.query("BEGIN") + // Temporary shadow table: public scheduler history is never changed. + await client.query("CREATE TEMP TABLE scheduler_runs(id integer PRIMARY KEY, finished_at timestamptz NOT NULL) ON COMMIT DROP") + await client.query(`INSERT INTO scheduler_runs SELECT i, now() - CASE WHEN i=1 THEN interval '60 days' ELSE i * interval '1 second' END FROM generate_series(1,20000) AS i`) + await client.query("ANALYZE scheduler_runs") + const cutoff = new Date(Date.now() - 30 * 86400_000).toISOString() + const plan = async () => JSON.stringify((await client.query("EXPLAIN (FORMAT JSON) SELECT id FROM scheduler_runs WHERE finished_at < $1", [cutoff])).rows) + assert.match(await plan(), /Seq Scan/) + const migration = readFileSync(new URL("../../drizzle/0013_scheduler_retention_index.sql", import.meta.url), "utf8") + await client.query(migration) + await client.query(migration) + assert.match(await plan(), /Index Scan|Bitmap Index Scan/) + assert.deepEqual((await client.query("SELECT id FROM scheduler_runs WHERE finished_at < $1", [cutoff])).rows, [{ id: 1 }]) + const index = (await client.query("SELECT indisvalid FROM pg_index WHERE indrelid = 'pg_temp.scheduler_runs'::regclass AND indexrelid::regclass::text LIKE '%idx_scheduler_runs_finished_at'")).rows + assert.deepEqual(index, [{ indisvalid: true }]) + console.log("Scheduler retention: actual migration/idempotence/valid index/selective plan/result preservation passed on 20,000 temporary rows") + } finally { + await client.query("ROLLBACK") + client.release() + } +} finally { + await pool.end() +} diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index e6ac3482..5777d81b 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -649,6 +649,7 @@ export const schedulerRuns = pgTable("scheduler_runs", { resultJson: jsonb("result_json"), }, (t) => [ index("idx_scheduler_runs_job_time").on(t.jobKey, t.startedAt), + index("idx_scheduler_runs_finished_at").on(t.finishedAt), ]) export const events = pgTable("events", { diff --git a/deploy/PRODUCTION-RETENTION-INDEX.md b/deploy/PRODUCTION-RETENTION-INDEX.md new file mode 100644 index 00000000..ef24340b --- /dev/null +++ b/deploy/PRODUCTION-RETENTION-INDEX.md @@ -0,0 +1,50 @@ +# Индекс истории планировщика — 2026-10-15 + +Диагностика production: scheduler_runs около1.06M строк/853МБ, retention DELETE после каждого задания, индекса finished_at нет. Наблюдалось ожидание IO/AioIoCompletion; EXPLAIN эквивалентного SELECT показал полный скан. Это подтверждает лишнюю нагрузку БД, но не доказывает причину каждого тайм-аута RouterOS. + +Миграция0013 создаёт индекс для новых установок и последующего старта backend. Для работающей установки ниже приведён отдельный online-вариант. **На production выполнен 2026-10-15, запуск в14:55:45 UTC, после разрешения владельца только на индекс и проверку нагрузки.** Создание индекса изменяет БД и временно добавляет нагрузку на CPU/диск. + +Подключение psql к текущему mmapp-postgres, без вывода пароля: + +```bash +docker exec -it mmapp-postgres sh -c 'exec psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' +``` + +Сначала проверить, что имя не занято другим или невалидным индексом: + +```sql +SELECT indexrelid::regclass, indisvalid, indisready, pg_get_indexdef(indexrelid) +FROM pg_index WHERE indexrelid = to_regclass('public.idx_scheduler_runs_finished_at'); +``` + +Если индекс отсутствует, выполнить **вне BEGIN/транзакции**: + +```sql +SET lock_timeout = '5s'; +SET statement_timeout = '10min'; +CREATE INDEX CONCURRENTLY idx_scheduler_runs_finished_at +ON public.scheduler_runs (finished_at); +RESET statement_timeout; +RESET lock_timeout; +``` + +При прерывании concurrent build не повторять вслепую с IF NOT EXISTS: сначала проверить indisvalid/indisready. Если остался невалидный индекс, отдельно согласовать его удаление и повторное построение. + +После успеха повторить проверку определения и валидности индекса, снять EXPLAIN условия retention и сравнить CPU/IO в нескольких окнах. При очень большой доле устаревших строк PostgreSQL может обоснованно выбрать Seq Scan; не отключать его принудительно. Срок хранения30 дней и код удаления не меняются. Индекс не удаляет записи и не требует перезапуска контейнеров. + +Удаление индекса, перезапуски и изменение ресурсов не входят в эту процедуру. Исправление карты — отдельный frontend-патч и само этим SQL не разворачивается. + +## Результат применения (2026-10-15) + +CREATE INDEX CONCURRENTLY завершён успешно; indisvalid/indisready=true. Повторная проверка в15:01:30 UTC:23МБ,697 использований индекса. EXPLAIN эквивалентного SELECT retention: Bitmap Heap Scan → Bitmap Index Scan idx_scheduler_runs_finished_at вместо Parallel Seq Scan. EXPLAIN ANALYZE DELETE не запускался. + +| Измерение | До индекса | После,14:56 UTC | После,15:00 UTC | +| --- | --- | --- | --- | +| CPU контейнера PostgreSQL, docker stats | 66,51% | 0,16% | 0,13% | +| CPU idle сервера, секундные vmstat | 0% | 74–94% | 94–97% | +| CPU iowait сервера, секундные vmstat | 29–34% | 0–5% | 0–1% | +| IO pressure some avg10 | 63,67% | 1,70% | 0,48% | + +Первая строка vmstat (среднее с загрузки ОС) исключена из сравнения. Замеры показывают устойчивое улучшение в пределах нескольких минут обычной работы, а не нагрузочную приёмку или гарантию отсутствия всех тайм-аутов. PostgreSQL/контейнеры не перезапускались; ручное удаление данных, изменение retention и параметров приложения не выполнялись. Действующий штатный retention продолжал работать. + +Автоматическая проверка фактического SQL миграции выполнена на отдельном тестовом Debian в контейнере mmapp-test-backend:20тыс. временных строк, повторный запуск, валидность индекса, смена плана и сохранение результата выборки. Тест не запускает bootstrap и не изменяет public-таблицы. Backend build и scoped ESLint прошли. [Структурированный протокол](../docs/ux-ui/acceptance/prod266-retention.json). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index dc4497b8..1317fe51 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное состояние 2026-10-15: 264 — лабораторная приёмка основного UX/UI-плана завершена в документированных границах стенда. Приняты1396 основных UI и221 отдельных context-сценарий;120 live default states30 экранов. Runtime backend258/frontend261. Непроверенные внешние группы сохранены в итоговом акте. Публикация main/2.0.0 отдельно разрешена владельцем; [описание релиза](releases/2.0.0.md). +Актуальное состояние 2026-10-15: Production266 — с отдельного разрешения создан online-индекс retention; нагрузка БД существенно снизилась в двух повторных замерах. Карта исправлена локально (265), frontend-патч ещё не развёрнут. Проверка трафика и причин остальных тайм-аутов продолжается отдельно. Лабораторная приёмка264 завершена в документированных границах стенда:1396 основных UI и221 context-сценарий;120 live default states30 экранов. Runtime стенда backend258/frontend261. Непроверенные внешние группы сохранены в итоговом акте. Production v2.0.0 подтверждён через UI; [описание релиза](releases/2.0.0.md). Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -38,6 +38,12 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Завершён2026-10-15 в границах предоставленного стенда:1396 UI/221 context,120 live states30 экранов, production builds/ReUI checklist и реальные интеграции. Ограничения — в [акте](ux-ui-acceptance-candidate.md); production-выпуск не выполнялся. | +## Production266 — online-индекс retention и проверка нагрузки (2026-10-15) + +После прямого разрешения владельца только на индекс и проверку нагрузки в14:55:45 UTC запущен CREATE INDEX CONCURRENTLY idx_scheduler_runs_finished_at ON public.scheduler_runs(finished_at). Завершён успешно, indisvalid/indisready=true. В15:01:30 UTC индекс23МБ,697 использований; план эквивалентного SELECT сменился с Parallel Seq Scan на Bitmap Index Scan. PostgreSQL CPU66,51% →0,16% →0,13%; секундные vmstat iowait29–34% →0–5% →0–1%, CPU idle0% →74–94% →94–97%. Наблюдения ограничены несколькими минутами обычной нагрузки и не доказывают устранение каждого тайм-аута RouterOS. + +Добавлены миграция0013 и индекс в Drizzle schema, автоматический SQL-тест в test:pg, [процедура](../deploy/PRODUCTION-RETENTION-INDEX.md) и [протокол](ux-ui/acceptance/prod266-retention.json). Фактический тест выполнен на отдельном тестовом Debian:20тыс. временных строк, повторный запуск миграции, валидный индекс, смена плана, сохранение результата; fixture откатан. Bootstrap/public-таблицы тест не меняет. Backend build/scoped ESLint passed. Production-контейнеры не перезапускались; ручного удаления данных и изменения срока retention не было. Приложение не развёртывалось, push отсутствует. Следующее действие: завершить независимую проверку расчётов трафика и подготовить обновление REST history; применение frontend-патчей к production требует отдельного разрешения. + ## Production265 — исчезновение узлов карты и первичная диагностика (2026-10-15) Через выданный read-only аккаунт подтверждён production v2.0.0. Карта отклоняет все4 устройства: API polledAt имеет штатный PostgreSQL timestamptz string (+00), frontend требует ISO. В локальной ветке исправлена только нормализация явного offset; отсутствующая зона и невозможная дата по-прежнему отклоняются. Сохранённый приватный ответ production теперь проходит:4 servers/25 GRE/0 probes. Регрессия сначала воспроизведена новым unit test; после исправления6 unit и13 map E2E прошли, TypeScript/scoped ESLint passed. Production не обновлялся; push этого исправления не выполнялся. diff --git a/docs/ux-ui/acceptance/prod266-retention.json b/docs/ux-ui/acceptance/prod266-retention.json new file mode 100644 index 00000000..1834a31a --- /dev/null +++ b/docs/ux-ui/acceptance/prod266-retention.json @@ -0,0 +1,40 @@ +{ + "date": "2026-10-15", + "authorization": "Only the retention index and load verification in production", + "productionAction": { + "startedAt": "2026-10-15T14:55:45Z", + "statement": "CREATE INDEX CONCURRENTLY idx_scheduler_runs_finished_at ON public.scheduler_runs (finished_at)", + "result": "success", + "valid": true, + "ready": true, + "verifiedAt": "2026-10-15T15:01:30Z", + "size": "23 MB", + "indexScans": 697, + "planBefore": "Parallel Seq Scan", + "planAfter": "Bitmap Heap Scan / Bitmap Index Scan idx_scheduler_runs_finished_at", + "restarts": false, + "manualDataDeletion": false, + "retentionPeriodChanged": false, + "applicationDeployed": false + }, + "observations": [ + { "period": "before", "postgresCpuPercent": 66.51, "hostIdlePercent": [0, 0], "hostIoWaitPercent": [29, 34], "ioPressureSomeAvg10Percent": 63.67 }, + { "period": "2026-10-15T14:56Z", "postgresCpuPercent": 0.16, "hostIdlePercent": [74, 94], "hostIoWaitPercent": [0, 5], "ioPressureSomeAvg10Percent": 1.70 }, + { "period": "2026-10-15T15:00Z", "postgresCpuPercent": 0.13, "hostIdlePercent": [94, 97], "hostIoWaitPercent": [0, 1], "ioPressureSomeAvg10Percent": 0.48 } + ], + "checks": { + "actualMigrationTest": "passed on separate test Debian; bundled from committed TypeScript source", + "temporaryRows": 20000, + "migrationIdempotence": "passed", + "selectivePlanAndResultPreservation": "passed", + "temporaryFixtureRolledBack": true, + "testBootstrapExecuted": false, + "backendBuild": "passed", + "scopedEslint": "passed" + }, + "limitations": [ + "Several-minute observations under ordinary workload, not a capacity benchmark", + "Not proof that every RouterOS timeout or interface stall has been resolved", + "Map fix remains local; traffic history refresh and numerical validation remain separate work" + ] +}