From 91b69ebe06ef20623b96916e46e92605721fc1b5 Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Sat, 15 Aug 2026 23:29:38 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=87=D0=B5=D1=80=D0=B5=D0=B4=D1=8C=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=B8=20=D1=81=D1=82=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B0,=20=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=B8=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На Windows процессы поднимаются через spawn и заново импортируют app.main. Хранилище задач создавалось на уровне модуля, поэтому каждый новый процесс при старте помечал чужие выполняющиеся задачи как сорванные - все три записи падали с "сервис был перезапущен во время обработки". Co-Authored-By: Claude Opus 5 (1M context) --- app/main.py | 10 +++++++++- app/version.py | 2 +- tests/test_concurrency.py | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index bda65bc..6831428 100644 --- a/app/main.py +++ b/app/main.py @@ -25,7 +25,12 @@ log = logging.getLogger("talkscore-asr") settings: Settings = load_settings() allowlist = parse_allowlist(settings.allow_ips) -store = JobStore(settings.data_dir / "jobs.db") + +# Хранилище создаётся не здесь, а при старте сервиса. На Windows процессы +# поднимаются через spawn и заново импортируют этот модуль: если бы очередь +# открывалась на уровне модуля, каждый новый процесс при старте помечал бы +# чужие выполняющиеся задачи как сорванные. +store: JobStore = None # type: ignore[assignment] def make_pipeline() -> Pipeline: """Своя копия моделей на каждого воркера: они не рассчитаны на общий доступ.""" return Pipeline( @@ -118,7 +123,10 @@ def _make_pool(count: int): @asynccontextmanager async def lifespan(app: FastAPI): + global store + (settings.data_dir / "uploads").mkdir(parents=True, exist_ok=True) + store = JobStore(settings.data_dir / "jobs.db") try: pipeline.warmup() _state["ready"] = True diff --git a/app/version.py b/app/version.py index 3d18726..906d362 100644 --- a/app/version.py +++ b/app/version.py @@ -1 +1 @@ -__version__ = "0.5.0" +__version__ = "0.6.0" diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 4540e9c..ca12c6c 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -257,3 +257,27 @@ class TestProcessPool: assert "не инициализирован" in str(exc) else: raise AssertionError("ожидалась ошибка о неинициализированном воркере") + + +class TestSpawnSafety: + """На Windows дочерний процесс заново импортирует app.main.""" + + def test_importing_main_does_not_open_store(self, tmp_path, monkeypatch): + """Иначе каждый новый процесс объявлял бы чужие задачи сорванными.""" + import sys + import types + + config = tmp_path / "config.toml" + config.write_text('[security]\ntoken="t"\n', encoding="utf-8") + monkeypatch.setenv("TALKSCORE_ASR_HOME", str(tmp_path)) + for name in ("sherpa_onnx", "onnx_asr", "onnxruntime"): + monkeypatch.setitem(sys.modules, name, types.ModuleType(name)) + for mod in [m for m in list(sys.modules) if m.startswith("app.")]: + del sys.modules[mod] + + import app.config as cfg + monkeypatch.setattr(cfg, "BASE_DIR", tmp_path) + import app.main as main + + assert main.store is None + assert not (tmp_path / "data" / "jobs.db").exists()