Release DB connections across slow I/O; fail fast on pool exhaustion - #262
Merged
Conversation
|
Warning Review limit reachedNext included review available in 26 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
A stress test with a single client exhausted the connection pool and cascaded to 500s. The root cause was handlers and the detection worker holding a pooled connection across slow non-DB work (LLM inference, NATS object I/O, image processing) — with a 10-connection pool and no acquire timeout, a few concurrent requests pinned every connection and later requests hung until the request timeout killed them. Release the connection across the slow phase everywhere it was held: - redact_detection: pre-flight DB work (auth, find detection/file, resolve the audit file row, resolve policies) under a scoped connection, dropped before the audit load, redaction inference, and object staging; re-acquired only for the commit transaction. - DetectionWorker::detect: manages its own connection per phase — reads under a connection, drops it across build/analyze/stage, re-acquires for the fenced finalize. - Avatar upload (account + workspace): authorize under a scoped connection, release before image processing and the NATS put. - Detection-audit read + redaction-review handlers: resolve the audit file row under a connection, release before the object-store load. To support this, split RunBlobStore's audit loading into a connection-bound resolve step (resolve_audit_file / resolve_review_file) and a connection-free load step (load_audit), deduplicating the object load/decode that was copied across two methods and removing the now-unused load_analyzed_document. Fail fast on pool exhaustion instead of hanging: - Default POSTGRES_CONNECTION_TIMEOUT to 10s (was unset = wait forever), below the request timeout, so a starved request fails promptly. - Map a pool wait timeout to a retryable 503 Service Unavailable (new ErrorKind::ServiceUnavailable) rather than a 500; a backend create/recycle timeout stays a 500. Rate limiting is intentionally left to the edge/infra (see issues); the server's job here is to hold shared resources briefly and degrade gracefully. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKk1YEG4tZ69jzYVQvQL8
With #257 merged, the deferred sixth offender: bulk_delete_files held one pooled connection across N sequential object purges (each a NATS delete). Drop the batch connection after the commit and re-acquire a short-lived connection per purge, so one connection is never pinned across the whole delete sequence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKk1YEG4tZ69jzYVQvQL8
martsokha
force-pushed
the
fix/release-db-conn-across-io
branch
from
August 31, 2026 18:09
f72f36b to
f5198ff
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The incident
A stress test with a single spammy client saturated the 10-connection DB pool and cascaded to
500s. Logs showedConnection acquisition took longer than expected elapsed=28sfollowed byrequest timeout exceeded→ 500.Root cause: handlers and the detection worker held a pooled connection across slow non-DB work (LLM inference, NATS object I/O, image processing). With a small pool and no acquire timeout, a few concurrent requests pinned every connection and everyone else hung until the request timeout killed them. An audit found this pattern in six places.
Release the connection across slow I/O
Every offender now scopes the pre-flight DB work, drops the connection before the slow phase, and re-acquires only for the final commit — the pattern already used by
upload_file,chat send_message, and the connection-sync service.redact_detection(handler)anonymize+ 4 object round-tripsDetectionWorker::detectanalyze+ NATS get/putbulk_delete_filespurge loopTo support this cleanly,
RunBlobStoreaudit loading is split into a connection-bound resolve step (resolve_audit_file/resolve_review_file) and a connection-free load step (load_audit). This also deduplicates the object load/decode logic that was copied acrossload_analyzed_documentandload_review_audit, and removes the now-unusedload_analyzed_document.Fail fast on pool exhaustion
POSTGRES_CONNECTION_TIMEOUTnow defaults to10s(was unset = wait forever), comfortably below the 30s request timeout, so a starved request fails promptly instead of hanging.503 Service Unavailable(newErrorKind::ServiceUnavailable) instead of a misleading500; a backend create/recycle timeout stays a500.Scope notes
Retry-Afterheader on the 503 would be a nice follow-up (the error path has no header mechanism today).Testing
Full gate green:
cargo check/clippy --all-targets --all-features --workspace -D warnings/fmt --check, unit tests (pool-config defaults updated to assert the finite timeout), doc build.🤖 Generated with Claude Code