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