Закрывает замечания ревью QA по PR #50: * #54: все чтения Store идут через Database.read() под тем же RLock, что транзакции; контракт «одно соединение, все операции под локом» задокументирован в db.py и README модуля. Устраняет TypeError/IndexError на общем соединении, грязные чтения и выдачу состояния, которое затем откатывается падающим apply. * #55 D-1: apply ловит BaseException — отмена/CancelledError/ KeyboardInterrupt вызывают Applier.revert(previous) и пишут failed в audit_log (без «висящей» записи staged); падение revert попадает в detail. * #55 D-2: set_geoip_schedule читает old_value внутри той же транзакции. Refs #20, Refs #54, Refs #55
This commit is contained in:
@@ -21,10 +21,29 @@ SQLite (WAL) в `/data/awg-dash.db`: правила маршрутизации,
|
||||
|
||||
- **Пустой `/data`** — каталог создаётся, миграции применяются, схема v1 появляется без ручных действий.
|
||||
- **Версия схемы** — при старте схема проверяется: новее ожидаемой или не мигрированная → `SchemaError` (без «автоотката»).
|
||||
- **Аудит** — каждая мутация пишет `actor/ts/action/entity/old_value/new_value` в `audit_log` в той же транзакции.
|
||||
- **Атомарность** — применяемое состояние проходит `validate → stage → apply`; падение внешнего применителя откатывает транзакцию БД и вызывает `Applier.revert(previous)`, `ApplyError.restored=True`.
|
||||
- **Аудит** — каждая мутация пишет `actor/ts/action/entity/old_value/new_value` в `audit_log` в той же транзакции; `old_value` читается внутри той же транзакции, поэтому конкурентные обновления не рвут цепочку `old→new`.
|
||||
- **Атомарность** — применяемое состояние проходит `validate → stage → apply`; падение внешнего применителя откатывает транзакцию БД и вызывает `Applier.revert(previous)`, `ApplyError.restored=True`. Ловится `BaseException`, поэтому отмена (`asyncio.CancelledError`), `KeyboardInterrupt` и `SystemExit` тоже дают `revert` и запись `failed` в журнале.
|
||||
- **Откат** — `ApplyEngine.rollback(plan_id)` возвращает состояние из снимка.
|
||||
|
||||
## Потокобезопасность (#54)
|
||||
|
||||
Контракт: **одно соединение на процесс, все операции под одним `RLock`**.
|
||||
|
||||
| Операция | API | Поведение |
|
||||
|---|---|---|
|
||||
| Мутация | `Database.transaction()` | `BEGIN IMMEDIATE` … `COMMIT`/`ROLLBACK` под локом |
|
||||
| Чтение | `Database.read()` | берёт тот же лок; ждёт завершения чужой транзакции |
|
||||
| Прямой доступ | `Database.connection` / `Store.raw()` | лок **не** берётся — только однопоточный код (миграции, тесты, отладка) |
|
||||
|
||||
Следствия, на которые можно опираться в API-слое (#19/#21):
|
||||
|
||||
- обычные чтения (`GET /rules`, `GET /mode`, …) не падают от конкурентных мутаций и не отдают перемешанный результат операторов;
|
||||
- читатель **не видит** незафиксированных строк другой транзакции;
|
||||
- во время `apply` наружу не видно состояние, которое затем будет откачено (AC «не частично применённая конфигурация»);
|
||||
- конкурентные запросы ждут, а не получают ошибку; потокобезопасность подтверждена `backend/tests/test_store_concurrency.py`.
|
||||
|
||||
WAL остаётся включённым: он даёт устойчивость к падению процесса и позволяет читать БД снаружи (`sqlite3` CLI) во время работы шлюза. Внутрипроцессного параллелизма чтений контракт не обещает: если позже понадобится параллельная пропускная способность, меняем контракт на «соединение на поток» (`threading.local`), а не полагаемся на неявное поведение.
|
||||
|
||||
## Пример
|
||||
|
||||
```python
|
||||
|
||||
Reference in New Issue
Block a user