fix(vision): /run del worker non blocca più l'event loop, timeout e semaforo (#34)

Decodifica + run_graph girano in asyncio.to_thread sotto asyncio.wait_for
(VISION_RUN_TIMEOUT_S, default 100s, sotto i 120s del backend) -> 504 del
worker allo scadere. /health resta leggero e risponde durante un calcolo
in corso. asyncio.Semaphore(VISION_MAX_CONCURRENCY, default 2): oltre ->
503 "worker busy". vision_service._call_worker inoltra 422/503/504 del
worker con il loro status e motivo invece di appiattirli su 502.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:39:28 +02:00
parent b024724c7e
commit b15037851b
3 changed files with 166 additions and 30 deletions
+16 -7
View File
@@ -21,6 +21,14 @@ from src.backend.models.orm.task import RecipeSubtask, RecipeTask
from src.backend.models.orm.vision import VisionReferenceImage, VisionResult
from src.backend.services import measurement_service
# Worker responses whose status and reason are forwarded as-is rather than
# flattened into a generic 502 - see `_call_worker`.
_FORWARDED_WORKER_STATUSES = frozenset({
status.HTTP_422_UNPROCESSABLE_CONTENT,
status.HTTP_503_SERVICE_UNAVAILABLE,
status.HTTP_504_GATEWAY_TIMEOUT,
})
def _is_measurable(value: object) -> bool:
"""A worker output counts as a quote only if it is a finite, non-bool number.
@@ -69,17 +77,18 @@ async def _call_worker(image_bytes: bytes, graph: dict) -> dict:
detail=f"the vision worker is unreachable: {exc}",
) from exc
if response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT:
# The worker already diagnosed a bad request (undecodable image,
# malformed graph) - forward its reason instead of flattening it
# into a generic 502.
# The worker already gave this fault a name - forward its status and
# reason instead of flattening every non-200 into a generic 502: 422 is a
# bad request (undecodable image, malformed graph), 503 is "busy, try
# again" (VISION_MAX_CONCURRENCY exceeded), 504 is its own run timeout
# (VISION_RUN_TIMEOUT_S, always tighter than this client's own 120s so
# the worker answers first).
if response.status_code in _FORWARDED_WORKER_STATUSES:
try:
detail = response.json().get("detail", response.text)
except ValueError:
detail = response.text
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=detail,
)
raise HTTPException(status_code=response.status_code, detail=detail)
if response.status_code != 200:
raise HTTPException(