fix: keep catalog IO off the live event loop
This commit is contained in:
parent
62166f77b8
commit
2894979a35
1 changed files with 93 additions and 57 deletions
|
|
@ -309,6 +309,14 @@ def create_app(
|
|||
# ---------------------------------------------------------------- health
|
||||
@app.get("/v1/health")
|
||||
async def health() -> Dict[str, Any]:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _health() -> Dict[str, Any]:
|
||||
gallery = (
|
||||
engine.identity_gallery()
|
||||
if getattr(engine.settings, "identity_backend", "") == "gallery"
|
||||
else None
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"model": {
|
||||
|
|
@ -320,11 +328,7 @@ def create_app(
|
|||
"catalog": {"version": engine.catalog_version(), "sku_count": engine.sku_count()},
|
||||
"identity": {
|
||||
"backend": getattr(engine.settings, "identity_backend", "matcher"),
|
||||
"gallery_views": (
|
||||
engine.identity_gallery().view_count()
|
||||
if getattr(engine.settings, "identity_backend", "") == "gallery" and engine.identity_gallery()
|
||||
else None
|
||||
),
|
||||
"gallery_views": gallery.view_count() if gallery is not None else None,
|
||||
},
|
||||
"ocr": {
|
||||
# Without this an operator could not tell a broken OCR from a shelf
|
||||
|
|
@ -332,19 +336,26 @@ def create_app(
|
|||
"engine": getattr(engine.ocr_engine, "name", "none"),
|
||||
"available": bool(getattr(engine.ocr_engine, "available", lambda: False)()),
|
||||
},
|
||||
"recording": recorder.stats() if recorder is not None else None,
|
||||
"sessions_active": len(sessions),
|
||||
"sessions_active": sum(session is not None for session in sessions.values()),
|
||||
"uptime_s": round(engine.uptime_s(), 1),
|
||||
}
|
||||
|
||||
return await loop.run_in_executor(None, _health)
|
||||
|
||||
# ---------------------------------------------------------------- catalog
|
||||
@app.get("/v1/catalog", dependencies=[Depends(authorize)])
|
||||
async def catalog() -> Dict[str, Any]:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _catalog() -> Dict[str, Any]:
|
||||
return {"catalog_version": engine.catalog_version(), "products": engine.catalog()}
|
||||
|
||||
return await loop.run_in_executor(None, _catalog)
|
||||
|
||||
@app.get("/v1/catalog/{sku_id}/image", dependencies=[Depends(authorize)])
|
||||
async def catalog_image(sku_id: str) -> Response:
|
||||
path = engine.catalog_image_path(sku_id)
|
||||
loop = asyncio.get_running_loop()
|
||||
path = await loop.run_in_executor(None, engine.catalog_image_path, sku_id)
|
||||
if not path:
|
||||
raise _error("NOT_FOUND", f"no catalog image for '{sku_id}'", 404)
|
||||
return FileResponse(path)
|
||||
|
|
@ -353,7 +364,8 @@ def create_app(
|
|||
async def update_stock(sku_id: str, body: StockUpdateBody) -> Dict[str, Any]:
|
||||
if body.delta == 0:
|
||||
raise _error("VALIDATION_ERROR", "delta must not be zero", 422)
|
||||
product = engine.update_stock(sku_id, body.delta)
|
||||
loop = asyncio.get_running_loop()
|
||||
product = await loop.run_in_executor(None, engine.update_stock, sku_id, body.delta)
|
||||
if product is None:
|
||||
raise _error("NOT_FOUND", f"unknown sku '{sku_id}'", 404)
|
||||
return {"status": "success", "product": product}
|
||||
|
|
@ -378,7 +390,7 @@ def create_app(
|
|||
@app.get("/v1/samples/{sample_id}", dependencies=[Depends(authorize)])
|
||||
async def get_sample(sample_id: str) -> Response:
|
||||
item = samples.get(sample_id)
|
||||
if not item or not os.path.exists(item["path"]):
|
||||
if not item or not await asyncio.get_running_loop().run_in_executor(None, os.path.exists, item["path"]):
|
||||
raise _error("NOT_FOUND", f"no sample '{sample_id}'", 404)
|
||||
return FileResponse(item["path"])
|
||||
|
||||
|
|
@ -640,11 +652,16 @@ def create_app(
|
|||
if settings.token and header and not bearer_matches(header, settings.token):
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
loop = asyncio.get_running_loop()
|
||||
async with sessions_lock:
|
||||
if len(sessions) >= settings.max_sessions:
|
||||
await websocket.close(code=1013)
|
||||
return
|
||||
session_id = str(uuid.uuid4())
|
||||
sessions[session_id] = None # reserve before any disk work
|
||||
session = None
|
||||
try:
|
||||
model_version = await loop.run_in_executor(None, engine.model_version)
|
||||
session = LiveSession(
|
||||
session_id,
|
||||
engine,
|
||||
|
|
@ -656,13 +673,21 @@ def create_app(
|
|||
tags=settings.live_tags,
|
||||
),
|
||||
)
|
||||
session.refresh_catalog()
|
||||
await loop.run_in_executor(None, session.refresh_catalog)
|
||||
catalog_version = session._catalog_version
|
||||
if settings.live_tags:
|
||||
session.enable_tags()
|
||||
await loop.run_in_executor(None, session.enable_tags)
|
||||
async with sessions_lock:
|
||||
sessions[session_id] = session
|
||||
except BaseException:
|
||||
if session is not None:
|
||||
session.close()
|
||||
async with sessions_lock:
|
||||
sessions.pop(session_id, None)
|
||||
raise
|
||||
try:
|
||||
await websocket.accept()
|
||||
send_lock = asyncio.Lock()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
async def send(payload: Dict[str, Any]) -> None:
|
||||
async with send_lock:
|
||||
|
|
@ -673,9 +698,9 @@ def create_app(
|
|||
"type": "hello",
|
||||
"protocol": 1,
|
||||
"session_id": session_id,
|
||||
"catalog_version": engine.catalog_version(),
|
||||
"catalog_version": catalog_version,
|
||||
"model": {
|
||||
"version": engine.model_version(),
|
||||
"version": model_version,
|
||||
"device": getattr(engine.settings, "device", None) or "auto",
|
||||
"imgsz": getattr(engine.settings, "imgsz", 640),
|
||||
},
|
||||
|
|
@ -683,6 +708,11 @@ def create_app(
|
|||
"limits": {"max_frame_bytes": MAX_FRAME_BYTES},
|
||||
}
|
||||
)
|
||||
except BaseException:
|
||||
session.close()
|
||||
async with sessions_lock:
|
||||
sessions.pop(session_id, None)
|
||||
raise
|
||||
|
||||
async def processing_loop() -> None:
|
||||
while not session.closed:
|
||||
|
|
@ -851,7 +881,14 @@ def create_app(
|
|||
# Base64 expands ~4/3. Reject the text before decoding it.
|
||||
if len(raw) > (MAX_FRAME_BYTES * 4) // 3 + 8:
|
||||
raise _error("PAYLOAD_TOO_LARGE", f"frame larger than {MAX_FRAME_BYTES} bytes", 413)
|
||||
frame_bytes = base64.b64decode(raw) if raw else b""
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
frame_bytes = (
|
||||
await loop.run_in_executor(None, lambda: base64.b64decode(raw, validate=True))
|
||||
if raw else b""
|
||||
)
|
||||
except (ValueError, base64.binascii.Error):
|
||||
raise _error("BAD_REQUEST", "frame_bytes_b64 is not valid base64", 400)
|
||||
if len(frame_bytes) > MAX_FRAME_BYTES:
|
||||
raise _error("PAYLOAD_TOO_LARGE", f"frame larger than {MAX_FRAME_BYTES} bytes", 413)
|
||||
request = FrameRequest(
|
||||
|
|
@ -868,7 +905,6 @@ def create_app(
|
|||
)
|
||||
if not request.frame_id:
|
||||
raise _error("BAD_REQUEST", "frame_id required", 400)
|
||||
loop = asyncio.get_running_loop()
|
||||
observation = await loop.run_in_executor(None, scans.observe_frame, request)
|
||||
return {"status": "success", "observation": observation.as_dict(), "session": session.snapshot()}
|
||||
except ObserveError as exc:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue