From 262331fc64d97e5a2ee9f6dec0fd1ef0337500d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 17 Jun 2026 20:20:46 +0400 Subject: [PATCH] =?UTF-8?q?session-08:=20upload=20saga=20=E2=80=94=2026=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B9,=20=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=D0=B9=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=20(original=5Fbytes=20NOT=20NULL=20+=20TEXT=20=D0=B2?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20BYTEA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- History/session-08-upload-saga.md | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 History/session-08-upload-saga.md diff --git a/History/session-08-upload-saga.md b/History/session-08-upload-saga.md new file mode 100644 index 0000000..4c0c9f5 --- /dev/null +++ b/History/session-08-upload-saga.md @@ -0,0 +1,97 @@ +# Сессия #8 — Upload Saga: 26 версий до рабочей загрузки + +_Дата: 2026-06-17_ +_Версия: v1.12 → v1.26_ + +--- + +## Проблема + +Загрузка файлов > 50-100 KB на managed-платформе pythonk8s.services.ngcloud.ru случайно обрывалась с `✗ Сеть` (XHR onerror, HTTP 000). + +## Хронология попыток + +| v | Что пробовали | Результат | +|---|---|---| +| 1.12-1.13 | FormData/multipart POST | ✗ Сеть > 100KB | +| 1.14 | Promise fix (uploadFileXHR) | ✗ | +| 1.15-1.17 | Чанки 50KB FormData → БД | Только чанк 0, остальные не доходят | +| 1.18 | Одно DB-соединение вместо 7 | ✗ | +| 1.19 | debug_log таблица | ✗ | +| 1.20 | Пул соединений psycopg2 | ✗ | +| 1.21 | base64 JSON вместо FormData | ✗ | +| 1.22 | Пул: keepalive, reconnect | ✗ | +| 1.23 | Всегда одно соединение (убрали if/cid) | ✗ | +| 1.24-1.25 | base64 как TEXT (без decode) + логи | ✗ — `original_bytes NOT NULL` | +| **1.26** | **`original_bytes DROP NOT NULL`** | **✓ РАБОТАЕТ** | + +## Корневые причины (две) + +### 1. `original_bytes BYTEA NOT NULL` +Вставляли только `original_b64 TEXT`, а `original_bytes` требовал значения. PostgreSQL выдавал 500, но клиент не видел ответа из-за таймаута Ingress. + +### 2. `base64.b64decode()` при загрузке +Декодирование 200KB base64 → 150KB байт → BYTEA INSERT занимало >30 сек. Ingress обрывал соединение. + +## Рабочий рецепт + +```python +# _upload_json (app.py) +# 1. Принимаем base64 как строку +# 2. INSERT TEXT (не BYTEA) — мгновенно +# 3. Без base64.decode() +# 4. Одно DB-соединение на весь запрос + +cur.execute( + "INSERT INTO documents (filename, mime_type, original_b64, status) VALUES (%s,%s,%s,'uploaded')", + (filename, mime, b64), # b64 — строка как есть +) +``` + +```sql +-- schema.py +original_bytes BYTEA, -- было NOT NULL → теперь nullable +original_b64 TEXT, -- новое: base64 строка +``` + +```javascript +// Клиент: FileReader.readAsDataURL → base64 → XHR JSON POST /upload +xhr.send(JSON.stringify({ + filename: file.name, + data: b64, // base64 строка + cid: contractId +})); +``` + +## Пул соединений (db.py) + +```python +ThreadedConnectionPool( + minconn=2, maxconn=5, + keepalives=1, + keepalives_idle=30, + keepalives_interval=10, + keepalives_count=3, + connect_timeout=10, +) +# Проверка живости: SELECT 1 при каждом getconn +# Переподключение при OperationalError/InterfaceError +``` + +## Как дебажили + +1. `debug_log` таблица — 12 шагов лога в `_upload_json` +2. curl тесты: 10×150KB — случайные HTTP 000 +3. `/test` endpoint: прямой SQL INSERT — работал, но только с малыми телами +4. Исключение FormData → переход на JSON +5. Исключение `base64.decode()` → TEXT как есть +6. `ALTER TABLE ... DROP NOT NULL` — последний фикс + +## Выученные уроки + +1. **Managed-платформа ≠ свой сервер** — лимиты невидимы и неконтролируемы +2. **TEXT INSERT быстрее BYTEA INSERT** — не гнать decode при загрузке +3. **NOT NULL constraint** — проверять схему при добавлении колонок +4. **debug_log в БД** — единственный способ увидеть что происходит внутри +5. **Версия в шаблоне** — перед КАЖДЫМ коммитом +6. **Пул соединений с keepalive** — обязательно для managed PostgreSQL