From f215e9e13f14badb4975a5af51e3dd97e03760a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 15 Jul 2026 11:03:54 +0400 Subject: [PATCH] docs: Sonnet SQLite question + review --- History/sonnet-question-sqlite-2026-07-15.md | 62 ++++++++++++++++++++ History/sonnet-review-sqlite-2026-07-15.md | 49 ++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 History/sonnet-question-sqlite-2026-07-15.md create mode 100644 History/sonnet-review-sqlite-2026-07-15.md diff --git a/History/sonnet-question-sqlite-2026-07-15.md b/History/sonnet-question-sqlite-2026-07-15.md new file mode 100644 index 0000000..a429d41 --- /dev/null +++ b/History/sonnet-question-sqlite-2026-07-15.md @@ -0,0 +1,62 @@ +# Вопрос для Sonnet (без ссылок на файлы) + +## Контекст + +Мигрировали сервис "Сверка Договоров" с ВМ на Flask (managed Python, Штурвал). +Код готов — 22 эндпоинта, вся логика внутри Flask: +- upload (XHR + progress), unzip, convert-doc +- классификация (LLM, ThreadPoolExecutor внутри Flask-потока для >10 файлов) +- группировка +- сравнение через SSE с heartbeat +- CRUD документов, supplements, промптов +- чат с LLM по результатам спецификации + +Архитектура: db/ (чистый CRUD) → services/ (бизнес-логика) → routes/ (Flask blueprints). + +## Проблема + +Сейчас БД — PostgreSQL. Но данные ВРЕМЕННЫЕ: +- При загрузке страницы cleanup() → DELETE ALL из всех таблиц (кроме prompts) +- Юзер загрузил файлы → обработал → закрыл → данные не нужны +- Файлы хранятся как base64 в БД (конфиденциально, должны умереть с сессией) + +Поднимать отдельный PostgreSQL-сервис в кластере ради временных данных — overkill. + +## Предложение + +Заменить PostgreSQL на SQLite (stdlib sqlite3): +- База — файл /tmp/contracts.db внутри Flask-контейнера +- connection.py переписать (~80 строк), db/*.py — мелкие правки (~30 строк) +- Остальной код (services, routes) — без изменений +- cleanup → os.remove() — атомарно, никаких DELETE, данные гарантированно исчезли +- Упал контейнер → файл исчез + +## Детальнее про конкурентность + +В НАШЕЙ архитектуре: +1. Classify запускается ВНУТРИ Flask-процесса как daemon-поток (threading.Thread), НЕ отдельный процесс +2. ThreadPoolExecutor(max_workers=4) внутри classify — 4 потока шлют запросы к LLM API +3. SSE-стриминг — читает БД, один writer (classify), один reader (SSE) +4. Обычно classify синхронный (≤10 файлов) — вообще один поток + +То есть: ОДИН процесс Flask, несколько потоков. Несколько процессов нет. + +## Что меняется в коде + +connection.py: +- psycopg2.pool → sqlite3 с thread-local соединениями +- threading.local() + get_conn() на каждый поток +- PRAGMA journal_mode=WAL +- check_same_thread=False + +db/*.py: +- %s → ? (sqlite-стиль placeholders) +- RETURNING * → lastrowid + отдельный SELECT +- ::jsonb → json.dumps() +- BOOLEAN → INTEGER (0/1) + +Остальное без изменений. + +## Вопрос + +Есть ли подводные камни с SQLite для этой конкретной архитектуры (один процесс, несколько потоков, WAL, thread-local connections)? diff --git a/History/sonnet-review-sqlite-2026-07-15.md b/History/sonnet-review-sqlite-2026-07-15.md new file mode 100644 index 0000000..705f7ff --- /dev/null +++ b/History/sonnet-review-sqlite-2026-07-15.md @@ -0,0 +1,49 @@ +# 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 при перезапуске контейнера — файл исчезает, что и нужно