Wrap the DB interaction in tokio::time::timeout and map timeouts to a distinct status, so a hang outside query execution still bounds the request.
Why
The SQL TIMEOUT clause governs query execution only — result decoding (take(0)), the try_from conversion loop, and actor round-trip latency are not covered.
Scope
- Wrap
db.query(stmt).await in tokio::time::timeout(QUERY_TIMEOUT + ~50ms grace, ...) at both sites (src/web/query.rs, src/web/item.rs). The small grace lets the engine's own clean abort fire first in the normal slow case; the wrapper only trips on a genuine hang.
- Map a timeout to a distinct status, not a 500:
- Search (
query.rs) currently returns generic web::Error (500) — add a timeout branch → 503/504.
- Item (
item.rs) — add a Timeout variant to the InnerError enum (src/web/item.rs:34) → 503/504.
- Log timeouts at
warn with the rendered SQL + bound filter values so slow filter combinations can be found and indexed.
Decision
- Pick 503 (with
Retry-After) vs 504 and use it consistently in both paths.
Depends on
Parent: #52
Wrap the DB interaction in
tokio::time::timeoutand map timeouts to a distinct status, so a hang outside query execution still bounds the request.Why
The SQL
TIMEOUTclause governs query execution only — result decoding (take(0)), thetry_fromconversion loop, and actor round-trip latency are not covered.Scope
db.query(stmt).awaitintokio::time::timeout(QUERY_TIMEOUT + ~50ms grace, ...)at both sites (src/web/query.rs,src/web/item.rs). The small grace lets the engine's own clean abort fire first in the normal slow case; the wrapper only trips on a genuine hang.query.rs) currently returns genericweb::Error(500) — add a timeout branch → 503/504.item.rs) — add aTimeoutvariant to theInnerErrorenum (src/web/item.rs:34) → 503/504.warnwith the rendered SQL + bound filter values so slow filter combinations can be found and indexed.Decision
Retry-After) vs 504 and use it consistently in both paths.Depends on
Parent: #52