запрос Соннету: загрузка с нуля, обход 64KB
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
# Запрос: загрузка файлов с нуля
|
||||||
|
|
||||||
|
**Задача:** надёжная загрузка docx/pdf/zip (до 50MB) через веб-интерфейс.
|
||||||
|
|
||||||
|
## Ресурсы
|
||||||
|
|
||||||
|
- **Flask** (managed pythonk8s.services.ngcloud.ru, деплой = git push)
|
||||||
|
- **PostgreSQL 17.6** (внутренний кластер `postgresqlk8s-master...svc.cluster.local`)
|
||||||
|
- **Redis** (внутренний `redisk8s...svc.cluster.local`, admin/aLITloRefJEiCPqUc2xB)
|
||||||
|
- **БД:** пул psycopg2 (2-10, keepalive), таблицы есть (`documents`, `contracts`, `supplements`)
|
||||||
|
- **Парсеры:** python-docx (DOCX), pdfplumber (PDF), zipfile (ZIP), textify.to_text()
|
||||||
|
- **LLM:** aillm.ru (gpt-oss-120b), HTTP/2
|
||||||
|
- **Клиент:** браузер, XHR POST, JSON body
|
||||||
|
|
||||||
|
## Что НЕ работает (43 версии)
|
||||||
|
|
||||||
|
- POST с телом > 64KB → nginx/Ingress рвёт TCP (HTTP 000, ReadTimeout)
|
||||||
|
- 10-50KB: 100% надёжно
|
||||||
|
- FormData, JSON/base64, чанки, Redis, retry — ничего не помогло
|
||||||
|
- 64KB — жёсткий предел платформы
|
||||||
|
|
||||||
|
## Что нужно
|
||||||
|
|
||||||
|
1. **Архитектура загрузки** — гарантированно обходящая лимит 64KB на тело запроса
|
||||||
|
2. **Прогресс на клиенте** — интерактивно (имя файла, проценты, таймер)
|
||||||
|
3. **Парсинг после загрузки** — извлечение параграфов, таблиц, стилей
|
||||||
|
4. **ZIP** — показывать содержимое, парсить файлы внутри
|
||||||
|
|
||||||
|
## Вопросы
|
||||||
|
|
||||||
|
1. Как обойти 64KB лимит тела запроса?
|
||||||
|
2. Чанки — на клиенте или сервере?
|
||||||
|
3. Redis — нужен или нет для этой задачи?
|
||||||
|
4. Как гарантировать целостность файла при чанковой загрузке?
|
||||||
|
5. Какая структура БД оптимальна для хранения результатов парсинга?
|
||||||
|
|
||||||
|
**Ответь кратко: архитектура (3-5 пунктов), потом отвечу на вопросы.**
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Запрос на анализ: XHR onerror («Сеть») при POST /upload с телом > 100KB
|
||||||
|
|
||||||
|
**Дата:** 2026-06-17
|
||||||
|
**Версия:** v1.41
|
||||||
|
**ТОЛЬКО АНАЛИЗ. КОД НЕ ПРАВИТЬ.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что поменялось в понимании
|
||||||
|
|
||||||
|
После просмотра текущего кода причина выглядит уже не как `request.get_data()` сама по себе.
|
||||||
|
|
||||||
|
Ветка `/upload` сейчас делает так:
|
||||||
|
|
||||||
|
1. читает всё тело через `request.get_data(as_text=True)`
|
||||||
|
2. выдёргивает `filename`, `data`, `cid` regex-ом
|
||||||
|
3. если `cid` пустой, синхронно создаёт контракт в PostgreSQL
|
||||||
|
4. затем синхронно вызывает `redis_client.push(...)`
|
||||||
|
5. только после этого возвращает `jsonify({"contract_id": ...})`
|
||||||
|
|
||||||
|
Самое подозрительное место — именно синхронный Redis push в запросе, а не чтение тела. Если Redis/DNS/сеть внутри кластера тормозит или подвисает, клиент уже видит `xhr.onerror` как «Сеть», хотя тело запроса успело уйти целиком.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущий код
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app.py
|
||||||
|
raw = request.get_data(as_text=True)
|
||||||
|
...
|
||||||
|
redis_client.push({"filename": filename, "b64": b64, "mime": mime, "cid": str(cid)})
|
||||||
|
return jsonify({"contract_id": str(cid)})
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# redis_client.py
|
||||||
|
_r = redis.Redis(
|
||||||
|
host=os.getenv("REDIS_HOST", "redisk8s....svc.cluster.local"),
|
||||||
|
port=int(os.getenv("REDIS_PORT", "6379")),
|
||||||
|
password=os.getenv("REDIS_PASS", "..."),
|
||||||
|
decode_responses=True,
|
||||||
|
socket_connect_timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def push(task: dict):
|
||||||
|
_get().rpush(UPLOAD_QUEUE, json.dumps(task, ensure_ascii=False))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Локальная гипотеза
|
||||||
|
|
||||||
|
`request.get_data()` не ломает соединение на 200KB. Запрос уже дошёл.
|
||||||
|
|
||||||
|
Соединение рвётся или уходит в таймаут на этапе синхронного `rpush()` в Redis, либо на первом обращении к Redis-клиенту, если DNS/соединение внутри кластера нестабильны. Это объясняет, почему:
|
||||||
|
|
||||||
|
- маленькие тела проходят чаще;
|
||||||
|
- сервер иногда успевает обработать и записать лог;
|
||||||
|
- `xhr.upload` показывает 100%, а затем приходит `onerror`;
|
||||||
|
- `curl` даёт плавающий `HTTP:000` / `ReadTimeout`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что надо проверить первым
|
||||||
|
|
||||||
|
Самый дешёвый дискриминирующий тест — замерить время именно вокруг `redis_client.push()` и временно обойти его для `/upload`.
|
||||||
|
|
||||||
|
Если ответ сразу стабилизируется, проблема подтверждается.
|
||||||
|
|
||||||
|
Если нет, следующая точка проверки — внешний таймаут/обрыв на ingress или gunicorn, но по текущему коду Redis выглядит ближе всего к критическому узлу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вывод
|
||||||
|
|
||||||
|
В v1.40 сместили фокус с парсинга тела на более поздний синхронный шаг. Сам `get_data()` теперь выглядит не как корень проблемы, а как просто первая стадия запроса. Корневой кандидат — блокирующий `redis_client.push()` в обработчике `/upload`.
|
||||||
Reference in New Issue
Block a user