Files
contracts/History/sessions/session-08-upload-saga.md
T

4.0 KiB
Raw Blame History

Сессия #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 обрывал соединение.

Рабочий рецепт

# _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 — строка как есть
)
-- schema.py
original_bytes BYTEA,        -- было NOT NULL → теперь nullable
original_b64   TEXT,          -- новое: base64 строка
// Клиент: FileReader.readAsDataURL → base64 → XHR JSON POST /upload
xhr.send(JSON.stringify({
    filename: file.name,
    data: b64,        // base64 строка
    cid: contractId
}));

Пул соединений (db.py)

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