# Sonnet Review: SQLite для Flask-миграции **Дата:** 2026-07-15 **Вердикт:** Жизнеспособно. Один реальный баг, остальное — конфигурация. --- ## ⛔ Критичный баг: inode split-brain **Симптом:** `ThreadPoolExecutor` переиспользует потоки. После classify 4 воркера остаются с открытыми соединениями. `cleanup()` делает `os.remove()` → новый `connect()` создаёт новый файл (новый inode). Но старые воркеры продолжают писать в старый inode (Linux не удаляет файл пока открыт fd). Воркеры пишут в призрак, Flask читает из нового пустого файла. **Решение:** глобальный ключ сессии в `get_conn()`: ```python _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 при перезапуске контейнера — файл исчезает, что и нужно