Files
contracts/History/sonnet-review-sqlite-2026-07-15.md
T

2.7 KiB

Sonnet Review: SQLite для Flask-миграции

Дата: 2026-07-15 Вердикт: Жизнеспособно. Один реальный баг, остальное — конфигурация.


Критичный баг: inode split-brain

Симптом: ThreadPoolExecutor переиспользует потоки. После classify 4 воркера остаются с открытыми соединениями. cleanup() делает os.remove() → новый connect() создаёт новый файл (новый inode). Но старые воркеры продолжают писать в старый inode (Linux не удаляет файл пока открыт fd). Воркеры пишут в призрак, Flask читает из нового пустого файла.

Решение: глобальный ключ сессии в get_conn():

_local = threading.local()
_db_inode = None

def get_conn():
    conn = getattr(_local, 'conn', None)
    if conn is None or getattr(_local, 'inode', None) != _db_inode:
        if conn: conn.close()
        conn = sqlite3.connect(DB_PATH, check_same_thread=False)
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=5000")
        _local.conn = conn
        _local.inode = _db_inode
    return conn

⚠️ Важные нюансы

WAL-сателлиты

os.remove("contracts.db") не удаляет .db-wal и .db-shm. SQLite следующего connect'а может применить старый WAL к новому файлу. Удалять все три.

Инициализация схемы после cleanup

После os.remove()connect() создаёт пустую БД. Нужен немедленный CREATE TABLE IF NOT EXISTS.

busy_timeout

4 воркера могут финишировать одновременно → параллельная запись. WAL сериализует, но default timeout = 0 (сразу SQLITE_BUSY). PRAGMA busy_timeout=5000.

Gunicorn workers = 1

Если workers > 1 — несколько процессов поделят один /tmp/contracts.db. cleanup() одного убьёт файл другого. Зафиксировать --workers 1.

Что не проблема

  • WAL + несколько потоков — штатный сценарий, readers не блокируют writers
  • Большие base64 в SQLite — работает
  • stdlib sqlite3 — ноль зависимостей
  • /tmp при перезапуске контейнера — файл исчезает, что и нужно