session-08: upload saga — 26 версий, решение найдено (original_bytes NOT NULL + TEXT вместо BYTEA)
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user