From aa1b801e94b814d24534d25f83b7434a08c44c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 25 Jun 2026 13:14:39 +0400 Subject: [PATCH] =?UTF-8?q?v1.0.178:=20classify=20=E2=86=92=20subprocess.P?= =?UTF-8?q?open=20(classify=5Fworker.py).=20=D0=98=D0=B7=D0=BE=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D1=81=D1=81=20=E2=80=94=20=D1=81=D0=B2=D0=BE?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D0=BD=D0=BD=D0=B5=D0=BA=D1=82=D1=8B=20?= =?UTF-8?q?=D0=BA=20=D0=91=D0=94.=20HTTP-=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=20=D0=BF=D1=80=D0=BE=D0=B4=D0=BE=D0=BB=D0=B6=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BE=D1=82=D0=B2=D0=B5=D1=87=D0=B0=D1=82=D1=8C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/classify_worker.py | 33 +++++++++++++++++++++++++++++++++ deploy/convert_server.py | 23 ++++++++++++++++++----- 2 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 deploy/classify_worker.py diff --git a/deploy/classify_worker.py b/deploy/classify_worker.py new file mode 100644 index 0000000..b5a5089 --- /dev/null +++ b/deploy/classify_worker.py @@ -0,0 +1,33 @@ +""" +classify_worker.py — Фоновый процесс классификации (Фаза async classify). + +Запускается через subprocess.Popen из convert_server.py. +Принимает batch_id как аргумент командной строки. +Не зависит от HTTP-сервера — свои коннекты к БД, своя память. + +ЗАЧЕМ: threading.Thread внутри HTTP-процесса делит коннекты к БД +с HTTP-потоком. При 50+ файлах ThreadPoolExecutor(4) + HTTP-поток +исчерпывают пул коннектов → сервер не отвечает → 502. + +subprocess.Popen создаёт отдельный питон-процесс со своей памятью +и своими коннектами. HTTP-сервер продолжает отвечать на batch-progress. +Процесс живёт пока classify не завершится, потом умирает. +""" + +import sys, os +sys.path.insert(0, os.path.dirname(__file__)) + +from services.classify import classify_batch + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 classify_worker.py ", file=sys.stderr) + sys.exit(1) + + batch_id = sys.argv[1] + try: + result = classify_batch(batch_id) + print(f"classify_worker DONE: {result}") + except Exception as e: + print(f"classify_worker FAILED: {e}", file=sys.stderr) + sys.exit(1) diff --git a/deploy/convert_server.py b/deploy/convert_server.py index 973cf8c..2adb857 100755 --- a/deploy/convert_server.py +++ b/deploy/convert_server.py @@ -3,7 +3,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn, TCPServer from urllib.parse import urlparse, parse_qs -import json, re, os +import json, re, os, sys # ── DB auto-seed ────────────────────────────────────────────────────────── from db import prompts as db_prompts @@ -234,24 +234,37 @@ class Handler(BaseHTTPRequestHandler): # ── POST /classify-batch ───────────────────────────────────────────── def _handle_classify_batch(self): + """ + POST /api/classify-batch — запустить классификацию в фоне. + + ЗАЧЕМ subprocess вместо threading.Thread: + ThreadPoolExecutor(4) + HTTP-поток делят коннекты к PostgreSQL. + При 50+ файлах пул исчерпывается → сервер не отвечает → 502. + Отдельный процесс — свои коннекты, своя память. + HTTP-сервер продолжает отвечать на batch-progress. + """ length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) batch_id = body.get("batch_id") if not batch_id: self._json({"ok": False, "error": "batch_id required"}, 400) return - from services.classify import classify_batch from db import documents as db_docs - import threading + import subprocess, os - # Посчитать сколько файлов — ответить сразу, классифицировать в фоне + # Посчитать сколько файлов — ответить сразу, classify в отдельном процессе pending = db_docs.list_pending(batch_id) total = len(pending) if total == 0: self._json({"ok": False, "error": "no pending documents"}, 400) return - threading.Thread(target=classify_batch, args=(batch_id,), daemon=True).start() + # Запустить classify_worker.py в отдельном процессе + worker_path = os.path.join(os.path.dirname(__file__), "classify_worker.py") + subprocess.Popen( + [sys.executable, worker_path, batch_id], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) self._json({"ok": True, "total": total}, 202) # ── POST /apply-groups ───────────────────────────────────────────────