session-08: upload saga — 26 версий, решение найдено (original_bytes NOT NULL + TEXT вместо BYTEA)

This commit is contained in:
“Naeel”
2026-06-17 20:20:46 +04:00
parent ac1f37cc9c
commit 262331fc64
+97
View File
@@ -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