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()