diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index cb4db71..3aed259 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -207,8 +207,12 @@ SVC_ID = 1 — фиксированный сервис (Болванка) - **`_op_results`** — `threading.Lock()` вокруг всех операций чтения/записи/cleanup. `pop(k, None)` вместо `del dict[k]` — безопасно при конкурентном доступе. -- **Advisory lock сценариев** — `pg_try_advisory_lock(hashtext('scenario:{clientId}:{stand}'))` - атомарно проверяет и захватывает блокировку. `finally: unlock_scenario()` в `run_scenario()`. +- **Partial unique index для сценариев** — `CREATE UNIQUE INDEX idx_one_running + ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'`. + Делает `INSERT INTO scenario_runs ... status='RUNNING'` атомарной проверкой: + вторая параллельная вставка получает unique violation → 409. + Это заменило сломанную реализацию на `pg_try_advisory_lock` (v1.2.20), + где lock брался на одном соединении, а unlock — на другом (из пула). - **Tracker** — `_atomic_update()`: read→mutate→write под одним `fcntl.flock`. Исключает lost-update между `add()` и `remove()` из разных воркеров gunicorn. diff --git a/HISTORY/2026-07-31-session.md b/HISTORY/2026-07-31-session.md index 8d43304..843fa9e 100644 --- a/HISTORY/2026-07-31-session.md +++ b/HISTORY/2026-07-31-session.md @@ -188,3 +188,56 @@ 11. **In-memory `_op_results` на воркер** — не shared между gunicorn-воркерами. Статус иногда читается из API fallback. Решение: Redis/БД для статусов. Отложено — низкая вероятность проблемы на практике (2 воркера, stickiness). + +--- + +## Повторный аудит Codex (2026-07-31, вторая итерация) + +Codex проверил исправления и нашёл **критические ошибки в моих же фиксах**: + +### ОШИБКА AI #1: Advisory lock сломан (v1.2.20) + +**Что я сделал:** `lock_check()` брал `pg_try_advisory_lock` на соединении `conn1`, +возвращал `True`, и `conn1` уходил обратно в пул. `unlock_scenario()` вызывал +`get_conn()` → получал `conn2` (другое соединение!) → unlock на `conn2` не снимал +lock с `conn1`. Плюс ранние `return` в `api_scenario_run.py` после успешного +`lock_check` вообще не вызывали unlock. + +**Почему ошибся:** не учёл что PostgreSQL advisory lock привязан к сессии (соединению), +а соединения возвращаются в пул. Передача соединения между `lock_check` и потоком +сценария требовала бы сложной оркестрации. + +**Как исправлено (v1.2.21):** заменён на **partial unique index** на уровне БД: +```sql +CREATE UNIQUE INDEX idx_one_running + ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'; +``` +Теперь `INSERT INTO scenario_runs ... status='RUNNING'` сам становится атомарной +проверкой — вторая вставка получает unique violation → 409 без гонок. +`lock_check` возвращён к простому SELECT (быстрая предпроверка для красивого 409). +`unlock_scenario` удалён полностью. `try/finally` из `run_scenario` убран. + +### ОШИБКА AI #2: escName не экранирует `"` (v1.2.19) + +**Что я сделал:** `def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'")` — +экранировал `\` и `'` для JS-строки, но забыл `"` для HTML-атрибута `onclick="..."`. + +**Почему ошибся:** фокусировался только на JS-контексте (строка в `'...'`), +не учёл что она внутри HTML-атрибута в `"..."`. + +**Как исправлено (v1.2.21):** добавлено `.replace(/"/g,'"')` в `escName`. + +### НОВАЯ находка Codex: instances.js:78 + +`instanceUid` в `onclick="toggleInstance('${i.instanceUid}')"` — теоретически уязвим, +но на практике UUID всегда `[a-f0-9-]+` → безопасен. Отмечен как низкий риск, +исправление не требуется. + +### ИТОГ + +| # | Слой | Статус | +|---|------|--------| +| Advisory lock | Python | ❌ СЛОМАН → ✅ partial unique index | +| escName `"` | JS | ❌ Неполный → ✅ добавлен `"` | +| instances.js onclick | JS | ⚠️ Низкий риск, UUID безопасен | +| `lock_check` fallback | Python | ✅ `True`→`False` при ошибке БД |