Repository navigation
perf(auth): offload synchronous token checks and throttle usage writes - #1380
Conversation
The bearer verifier chain (PAT lookup, OAuth2 access-token check with client/user/ingress validation) runs entirely synchronous SQLite work inside the async request path, and every successful authentication also wrote last_used_at. On the shared single writer connection that stalls an async worker whenever the store is contended and adds one write per request. Run the whole chain on the blocking pool from one spawn_blocking in authenticate() (and the MCP public-ingress check likewise): the token moves into the task and is dropped with it, every failure — including a lost blocking task — stays a rejection, and no check is skipped. last_used_at writes now carry a 60-second floor implemented as a single conditional UPDATE in the store (no cache, no read-modify-write race); the field is display/audit metadata, and expiry/revocation are still validated on every authentication. The verifier bodies become inherent sync functions with the async trait wrappers retained; signatures take Arc<Config> so the chain can move onto the blocking task without cloning Config. Regression proof: with the store's writer mutex held from the runtime thread, authentication on a current-thread runtime still completes — the pre-fix inline behavior deadlocks that runtime outright (the test's own deadline cannot fire, which is the failure mode itself). The computer post-observation MCP test drove its call future once and assumed admission had already happened; it now drives the call until admission with the same exactly-one assertion.
# Conflicts: # crates/webcodex-store/src/db_tests.rs
swjnxyf
left a comment
There was a problem hiding this comment.
Reviewed blocking-pool PAT/OAuth2 authentication, fail-closed join/error paths, public MCP ingress fence, the conditional 60-second last_used_at write floor, and changed call sites. Integrated latest main, resolving the shared Store test insertion without dropping #1379's bounded purge regression. Fixed the three-platform production warnings by fencing obsolete async TokenVerifier adapters to tests in follow-up ddd90db. On wkp: 325 Store tests, 575 auth-related Server tests, and 13 Computer post-observation regressions all passed; cargo fmt/diff checks passed; production dogfood check completed with 0 warnings and Cargo exit 0. No blocking issues remain; approving for the maintainer-authorized squash merge without waiting for the updated-head hosted CI.
Problem
The bearer verifier chain (PAT lookup, OAuth2 access-token check with client/user validation) runs entirely synchronous SQLite work inside the async request path — on this project's store that means one shared writer
Mutex<Connection>. Every successful authentication also wrotelast_used_at, so each authenticated request performed at least one write while holding that connection, stalling the async worker thread for the duration whenever the store was contended.Found in a full code audit of v0.5.0. No authorization behavior depends on it:
last_used_atis display/audit metadata (token-management listing); expiry and revocation are re-validated from their own columns on every authentication.Change
authenticate()now runs the whole verifier chain in onespawn_blockingtask. The token moves into the task and is dropped with it (never logged); every failure — including a lost or panicked blocking task — remains a rejection; no check is skipped. The MCP public-ingress check (authenticate_mcp) gets the same treatment. Verifier bodies become inherent synchronous functions; the async trait wrappers remain for existing callers/tests.last_used_atwrites now carry a 60-second floor, implemented as a single conditionalUPDATEin the store (AND (last_used_at IS NULL OR last_used_at <= ts - 60)): no cache, no read-modify-write race, at most one write per credential per window.LAST_USED_UPDATE_FLOOR_SECSis a named constant documenting the contract.Arc<Config>so the chain can move onto the blocking task without cloningConfig.Validation
authenticate_database_work_runs_on_the_blocking_pool(current-thread runtime): with the store's writer mutex held from the runtime thread, authentication still completes. With the previous inline behavior this exact scenario deadlocks the runtime thread outright — the test's own deadline cannot even fire, which is the failure mode itself; the fixed code completes in ~0.1s.last_used_at_updates_are_floored_to_one_write_per_window: first observation writes, an update 30s later is skipped, past the 60s floor the value moves again — for api keys and OAuth access tokens.cargo test --locked -p webcodex: 3,902 passed, 0 failed;cargo test --locked -p webcodex-store --lib: 324 passed, 0 failed; clippy-D clippy::correctnessandcargo fmt --checkclean.Compatibility
No auth decisions, wire formats, or persisted schemas change. Error classification for purged/stale tokens is unchanged (all failures deny). Authentication now hops to the blocking pool, which adds a scheduling handoff per authentication — bounded by the existing blocking-pool size, and the win is removing writer-lock stalls from async workers.