Skip to content

Release DB connections across slow I/O; fail fast on pool exhaustion - #262

Merged
martsokha merged 2 commits into
mainfrom
fix/release-db-conn-across-io
Aug 31, 2026
Merged

Release DB connections across slow I/O; fail fast on pool exhaustion#262
martsokha merged 2 commits into
mainfrom
fix/release-db-conn-across-io

Conversation

@martsokha

@martsokha martsokha commented Aug 31, 2026

Copy link
Copy Markdown
Member

The incident

A stress test with a single spammy client saturated the 10-connection DB pool and cascaded to 500s. Logs showed Connection acquisition took longer than expected elapsed=28s followed by request 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.

Path Was held across Now
redact_detection (handler) audit load + LLM anonymize + 4 object round-trips connection released before all of it
DetectionWorker::detect LLM analyze + NATS get/put manages its own connection per phase
Avatar upload (account + workspace) image decode/encode + NATS put authorize, then release before processing
Detection-audit reads + redaction review NATS get resolve file row, release, then load
bulk_delete_files purge loop N sequential NATS deletes under one connection release batch connection; re-acquire a short-lived one per purge

To support this cleanly, RunBlobStore audit 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 across load_analyzed_document and load_review_audit, and removes the now-unused load_analyzed_document.

Fail fast on pool exhaustion

  • POSTGRES_CONNECTION_TIMEOUT now defaults to 10s (was unset = wait forever), comfortably below the 30s request timeout, so a starved request fails promptly instead of hanging.
  • A pool wait timeout now maps to a retryable 503 Service Unavailable (new ErrorKind::ServiceUnavailable) instead of a misleading 500; a backend create/recycle timeout stays a 500.

Scope notes

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

@martsokha martsokha added bug something isn't working as intended server API handlers, middleware, auth postgres ORM, models, queries, migrations labels Aug 31, 2026
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 26 minutes.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 16bcc530-f82a-43e0-b02f-237da30e1868

📥 Commits

Reviewing files that changed from the base of the PR and between 222838a and f5198ff.

📒 Files selected for processing (13)
  • crates/nvisy-postgres/src/client/pg_config.rs
  • crates/nvisy-postgres/src/error.rs
  • crates/nvisy-postgres/src/lib.rs
  • crates/nvisy-server/src/handler/accounts.rs
  • crates/nvisy-server/src/handler/detection_audits.rs
  • crates/nvisy-server/src/handler/detections.rs
  • crates/nvisy-server/src/handler/error/http_error.rs
  • crates/nvisy-server/src/handler/error/pg_error.rs
  • crates/nvisy-server/src/handler/files.rs
  • crates/nvisy-server/src/handler/redactions.rs
  • crates/nvisy-server/src/handler/workspaces.rs
  • crates/nvisy-server/src/service/detection/worker.rs
  • crates/nvisy-server/src/service/run_blob_store.rs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

martsokha and others added 2 commits August 31, 2026 20:07
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
martsokha force-pushed the fix/release-db-conn-across-io branch from f72f36b to f5198ff Compare August 31, 2026 18:09
@martsokha martsokha self-assigned this Aug 31, 2026
@martsokha
martsokha merged commit cfd44c9 into main Aug 31, 2026
9 checks passed
@martsokha
martsokha deleted the fix/release-db-conn-across-io branch August 31, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug something isn't working as intended postgres ORM, models, queries, migrations server API handlers, middleware, auth

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant