From 79cd25a76223f3f78a4a6a98d3b5ddb20de00ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 14 Jul 2026 09:15:44 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20SSE=20=E2=80=94=20=D0=BE=D1=82=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=20GeneratorExit=20=D0=BF=D1=80=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api_bp.py: try/except GeneratorExit после каждого yield в generate() - при F5 во время обработки поток освобождается, остальные файлы не обрабатываются - bump: 0.0.17 → 0.0.18 --- History/2026-07-14-sse-generator-exit.md | 48 ++++++++++++++++++++++++ site/app.py | 2 +- site/routes/api_bp.py | 13 +++++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 History/2026-07-14-sse-generator-exit.md diff --git a/History/2026-07-14-sse-generator-exit.md b/History/2026-07-14-sse-generator-exit.md new file mode 100644 index 0000000..0f46b84 --- /dev/null +++ b/History/2026-07-14-sse-generator-exit.md @@ -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"` diff --git a/site/app.py b/site/app.py index 0053bd0..a1cc45a 100644 --- a/site/app.py +++ b/site/app.py @@ -20,7 +20,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.17" +VERSION = "0.0.18" def create_app(): diff --git a/site/routes/api_bp.py b/site/routes/api_bp.py index c38585d..7194d7f 100644 --- a/site/routes/api_bp.py +++ b/site/routes/api_bp.py @@ -59,7 +59,11 @@ def process_stream(sid): all_mapping = {} for idx, (fname, content) in enumerate(files): # Отправляем: начали файл - yield f"event: start\ndata: {json.dumps({'idx': idx, 'name': fname, 'total': total})}\n\n" + try: + 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) with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: for name in zf.namelist(): @@ -73,8 +77,11 @@ def process_stream(sid): all_mapping[parts[1]] = parts[2] elif not name.endswith("/"): all_results.append((name, zf.read(name))) - # Отправляем: закончили файл - yield f"event: done\ndata: {json.dumps({'idx': idx, 'name': fname, 'total': total})}\n\n" + # Отправляем: закончили файл (если клиент ушёл — освобождаем поток) + try: + 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 "" final_zip = build_zip(all_results)