fix: SSE — отлов GeneratorExit при отключении клиента
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
- api_bp.py: try/except GeneratorExit после каждого yield в generate() - при F5 во время обработки поток освобождается, остальные файлы не обрабатываются - bump: 0.0.17 → 0.0.18
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# SSE: отлов отключения клиента, v0.0.18
|
||||||
|
|
||||||
|
**Дата:** 2026-07-14
|
||||||
|
**Версия:** 0.0.17 → 0.0.18
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
После 2-3 F5 во время обработки — загрузка падает с «Ошибка загрузки: Сеть».
|
||||||
|
Причина: утечка потоков.
|
||||||
|
|
||||||
|
### Механика
|
||||||
|
|
||||||
|
1. `process_stream` создаёт SSE-генератор, занимающий поток Waitress (всего 8)
|
||||||
|
2. `obfuscate_files()` выполняется ~30 сек (LLM-запросы)
|
||||||
|
3. Пользователь делает F5 → TCP-соединение рвётся
|
||||||
|
4. Генератор **не знает** что клиент отключился — продолжает обрабатывать файлы
|
||||||
|
5. Поток занят, не освобождается
|
||||||
|
6. 2-3 F5 → все 8 потоков заняты → новые запросы не принимаются → «Сеть»
|
||||||
|
|
||||||
|
### Почему WSGI не отключает
|
||||||
|
|
||||||
|
`yield` в генераторе пишет в буфер WSGI, а не в сокет. Буфер никуда не уходит
|
||||||
|
(клиент отключён), но генератор об этом не узнаёт — `GeneratorExit` не кидается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Добавлен `try/except GeneratorExit` после каждого `yield` в `generate()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
yield f"event: done..."
|
||||||
|
except GeneratorExit:
|
||||||
|
return # клиент отключился, освобождаем поток, НЕ обрабатываем остальные файлы
|
||||||
|
```
|
||||||
|
|
||||||
|
Waitress при отключении клиента кидает `GeneratorExit` при попытке записи в
|
||||||
|
закрытый сокет. Генератор ловит → выходит → поток освобождается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменённые файлы
|
||||||
|
|
||||||
|
- `site/routes/api_bp.py` — `try/except GeneratorExit` в `generate()`
|
||||||
|
- `site/app.py` — `VERSION = "0.0.18"`
|
||||||
+1
-1
@@ -20,7 +20,7 @@ if _sys_path_root not in sys.path:
|
|||||||
sys.path.insert(0, _sys_path_root)
|
sys.path.insert(0, _sys_path_root)
|
||||||
|
|
||||||
# Версия приложения (меняется при изменениях)
|
# Версия приложения (меняется при изменениях)
|
||||||
VERSION = "0.0.17"
|
VERSION = "0.0.18"
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ def process_stream(sid):
|
|||||||
all_mapping = {}
|
all_mapping = {}
|
||||||
for idx, (fname, content) in enumerate(files):
|
for idx, (fname, content) in enumerate(files):
|
||||||
# Отправляем: начали файл
|
# Отправляем: начали файл
|
||||||
|
try:
|
||||||
yield f"event: start\ndata: {json.dumps({'idx': idx, 'name': fname, 'total': total})}\n\n"
|
yield f"event: start\ndata: {json.dumps({'idx': idx, 'name': fname, 'total': total})}\n\n"
|
||||||
|
except GeneratorExit:
|
||||||
|
return # клиент отключился
|
||||||
|
|
||||||
zip_data, _csv = obfuscate_files([(fname, content, "")], llm_client=llm)
|
zip_data, _csv = obfuscate_files([(fname, content, "")], llm_client=llm)
|
||||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||||
for name in zf.namelist():
|
for name in zf.namelist():
|
||||||
@@ -73,8 +77,11 @@ def process_stream(sid):
|
|||||||
all_mapping[parts[1]] = parts[2]
|
all_mapping[parts[1]] = parts[2]
|
||||||
elif not name.endswith("/"):
|
elif not name.endswith("/"):
|
||||||
all_results.append((name, zf.read(name)))
|
all_results.append((name, zf.read(name)))
|
||||||
# Отправляем: закончили файл
|
# Отправляем: закончили файл (если клиент ушёл — освобождаем поток)
|
||||||
|
try:
|
||||||
yield f"event: done\ndata: {json.dumps({'idx': idx, 'name': fname, 'total': total})}\n\n"
|
yield f"event: done\ndata: {json.dumps({'idx': idx, 'name': fname, 'total': total})}\n\n"
|
||||||
|
except GeneratorExit:
|
||||||
|
return # клиент отключился, НЕ обрабатываем остальные файлы
|
||||||
|
|
||||||
csv_str = build_mapping_csv(all_mapping) if all_mapping else ""
|
csv_str = build_mapping_csv(all_mapping) if all_mapping else ""
|
||||||
final_zip = build_zip(all_results)
|
final_zip = build_zip(all_results)
|
||||||
|
|||||||
Reference in New Issue
Block a user