Закрывает замечания ревью 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:
@@ -5,9 +5,25 @@
|
||||
Каталог создаётся автоматически, поэтому на пустом ``/data`` схема v1
|
||||
поднимается без ручных действий.
|
||||
|
||||
Соединение открывается в режиме WAL: читатели не блокируют писателя,
|
||||
``synchronous=NORMAL`` достаточен для WAL (риск потери только последних
|
||||
транзакций при падении операционной системы, БД при этом не разрушается).
|
||||
Соединение открывается в режиме WAL: ``synchronous=NORMAL`` достаточен для WAL
|
||||
(риск потери только последних транзакций при падении операционной системы, БД
|
||||
при этом не разрушается).
|
||||
|
||||
Контракт потокобезопасности (#54): соединение одно на процесс, все операции —
|
||||
и записи (:meth:`Database.transaction`), и чтения (:meth:`Database.read`) —
|
||||
выполняются под одним ``RLock``. Поэтому:
|
||||
|
||||
* результаты операторов из разных потоков не перемешиваются (нет
|
||||
``TypeError``/``IndexError``);
|
||||
* читатель не видит незафиксированных строк чужой транзакции и состояния,
|
||||
которое затем будет откачено (падение внешнего применителя);
|
||||
* конкурентные запросы ждут, не падают.
|
||||
|
||||
WAL при этом остаётся: он даёт устойчивость к падению процесса и позволяет
|
||||
читать БД снаружи (``sqlite3`` CLI) во время работы шлюза. Если позже
|
||||
понадобится параллельная пропускная способность чтений, контракт меняется на
|
||||
«соединение на поток» (``threading.local``) — это отдельное решение, не
|
||||
молчаливое поведение.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -83,12 +99,18 @@ class Database:
|
||||
|
||||
@property
|
||||
def connection(self) -> sqlite3.Connection:
|
||||
"""Открытое соединение (для сервисного слоя)."""
|
||||
"""Прямой доступ к соединению — только для однопоточного кода.
|
||||
|
||||
Рантайм API/сервисного слоя использует :meth:`read` (чтения) и
|
||||
:meth:`transaction` (мутации): они берут общий лок и гарантируют
|
||||
согласованность результата (#54).
|
||||
"""
|
||||
return self._conn
|
||||
|
||||
@property
|
||||
def journal_mode(self) -> str:
|
||||
row = self._conn.execute("PRAGMA journal_mode").fetchone()
|
||||
with self._lock:
|
||||
row = self._conn.execute("PRAGMA journal_mode").fetchone()
|
||||
return str(row[0]) if row is not None else ""
|
||||
|
||||
@property
|
||||
@@ -97,7 +119,8 @@ class Database:
|
||||
|
||||
@property
|
||||
def schema_version(self) -> int:
|
||||
return current_version(self._conn)
|
||||
with self._lock:
|
||||
return current_version(self._conn)
|
||||
|
||||
# -- схема ----------------------------------------------------------
|
||||
def migrate(self) -> MigrationReport:
|
||||
@@ -109,9 +132,10 @@ class Database:
|
||||
|
||||
def verify_schema(self) -> None:
|
||||
"""Проверить соответствие фактической схемы ожидаемой версии."""
|
||||
verify_schema(self._conn)
|
||||
with self._lock:
|
||||
verify_schema(self._conn)
|
||||
|
||||
# -- транзакции -----------------------------------------------------
|
||||
# -- транзакции и чтения --------------------------------------------
|
||||
@contextmanager
|
||||
def transaction(self) -> Iterator[sqlite3.Connection]:
|
||||
"""Транзакция ALL-OR-NOTHING (``BEGIN IMMEDIATE`` + rollback при ошибке)."""
|
||||
@@ -125,6 +149,18 @@ class Database:
|
||||
else:
|
||||
self._conn.commit()
|
||||
|
||||
@contextmanager
|
||||
def read(self) -> Iterator[sqlite3.Connection]:
|
||||
"""Согласованное чтение под общим локом (#54).
|
||||
|
||||
Ждёт завершения чужой транзакции и потому не видит незафиксированных
|
||||
строк и состояния, которое затем будет откачено. Внутри уже открытой
|
||||
транзакции того же потока лок реентерабелен, поэтому читать можно и из
|
||||
неё.
|
||||
"""
|
||||
with self._lock:
|
||||
yield self._conn
|
||||
|
||||
def close(self) -> None:
|
||||
with self._lock:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user