Spaces:
Sleeping
Sleeping
Alessandro Tomassini
deploy(hf): overlay README/Dockerfile da huggingface/, senza docs/binari/model
c42a5a1 | """Il route /api/detect non deve bloccare l'event loop asincrono. | |
| Regressione: /api/detect era ``async def`` ma chiamava ``analyze_text`` (lavoro | |
| CPU/GPU pesante) direttamente nella coroutine, bloccando l'intero event loop per | |
| tutta la durata dell'analisi. Conseguenza lato UI: durante la PRIMA analisi (con | |
| i modelli ancora da caricare, quindi lunga) il monitor di sistema restava vuoto | |
| perché /api/system/stats non poteva essere servito finché /api/detect non | |
| finiva. Gli altri endpoint pesanti offloadano già su threadpool con | |
| run_in_executor; questo test verifica che anche /api/detect lo faccia. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import time | |
| import httpx | |
| from config.runtime.settings import get_settings | |
| from web.app import create_app | |
| from web.services.analysis import AnalysisService | |
| from web.services.jobs import JobManager | |
| from web.services.registry import ServiceRegistry | |
| def test_detect_does_not_block_event_loop(monkeypatch): | |
| app = create_app() | |
| # Replica lo stato del lifespan SENZA il thread di warmup (non serve qui: | |
| # analyze_text è patchato per simulare un'analisi lenta). | |
| settings = get_settings() | |
| app.state.registry = ServiceRegistry(settings) | |
| app.state.jobs = JobManager() | |
| # Simula la prima analisi lenta (caricamento modelli) come lavoro SINCRONO | |
| # bloccante, poi delega alla vera analyze_text per una risposta valida. | |
| original = AnalysisService.analyze_text | |
| def slow_analyze(self, *args, **kwargs): | |
| time.sleep(0.6) | |
| return original(self, *args, **kwargs) | |
| monkeypatch.setattr(AnalysisService, "analyze_text", slow_analyze) | |
| order: list[str] = [] | |
| async def scenario(): | |
| transport = httpx.ASGITransport(app=app) | |
| async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac: | |
| async def do_detect(): | |
| r = await ac.post( | |
| "/api/detect", | |
| json={"text": "CIG 7654321000", "min_confidence": 0.7, "layers": ["rules"]}, | |
| ) | |
| assert r.status_code == 200 | |
| order.append("detect") | |
| async def do_stats(): | |
| # Lascia partire /api/detect, poi interroga il monitor mentre | |
| # l'analisi è in corso. | |
| await asyncio.sleep(0.1) | |
| r = await ac.get("/api/system/stats") | |
| assert r.status_code == 200 | |
| order.append("stats") | |
| await asyncio.gather(do_detect(), do_stats()) | |
| asyncio.run(scenario()) | |
| # Se l'event loop NON è bloccato, /api/system/stats risponde durante | |
| # l'analisi e completa PRIMA di /api/detect. | |
| assert order[0] == "stats", ( | |
| f"event loop bloccato durante /api/detect (ordine={order}): il monitor " | |
| "non riceve gli stats finché l'analisi non termina" | |
| ) | |