Skip to content

Harden DB session cleanup against task cancellation (DEV-1861)#908

Open
adavyas wants to merge 4 commits into
plastic-labs:mainfrom
adavyas:adavya/dev-1861
Open

Harden DB session cleanup against task cancellation (DEV-1861)#908
adavyas wants to merge 4 commits into
plastic-labs:mainfrom
adavyas:adavya/dev-1861

Conversation

@adavyas

@adavyas adavyas commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens the DB session cleanup path against task cancellation — one of the causes of the idle in transaction zombie-connection leak in DEV-1861, not all of them (see Coverage below). A pg_stat_activity snapshot showed 101 backends stuck idle in transaction via Supavisor (93 for 1–24h, worst 21h27m) across 34 tenant schemas, accruing ~5/hour — draining the pool, pinning the xmin horizon (vacuum starvation), and blocking DDL.

Root cause

Cancellation (client disconnect, stream abort, request-scoped timeout, worker shutdown) is delivered at an await. Session cleanup lived in a finally:

finally:
    await db.rollback()   # cancel re-delivered HERE
    await db.close()      # never runs
  • except Exception never catches cancellation (CancelledError is a BaseException), so only the finally covered it.
  • But the finally's own awaits are cancellable: a re-delivered CancelledError interrupts ROLLBACK before it reaches Postgres and skips close(), orphaning the connection mid-BEGIN.
  • Supavisor (transaction mode) does not reset the orphaned backend on disconnect → it parks idle in transaction indefinitely.

Where the re-delivery comes from. A single Task.cancel() delivers CancelledError exactly once, so the old finally would have completed on its own — the bug needs cancellation re-delivered during cleanup. anyio provides exactly that: CancelScope._deliver_cancellation reschedules itself via loop.call_soon on every event-loop tick for as long as the task is inside the scope, and Starlette's BaseHTTPMiddleware (registered for every request in src/main.py) wraps each request in an anyio task group. So the storm is real and reachable on the whole API path.

This is not the held-session-across-external-call antipattern — the dominant path (post-Turbopuffer batch fetch) correctly opens the DB session after the embedding/vector call. The leak is purely in the cleanup path.

Coverage — what this does and does not fix

Class Covered?
Cancelled request / task, process still alive (client disconnect, stream abort, timeout, graceful worker shutdown) ✅ this PR
Process death — SIGKILL, OOM-kill, machine replacement/restart not fixable in Python

On process death no cleanup code runs at all: the socket to Supavisor just drops, and Supavisor in transaction mode still does not reset the server backend. Given the observed age distribution (93 backends aged 1–24h, worst 21h27m), an unknown but plausibly large share of the snapshot is this second class. The only thing that reclaims those is the server-side idle_in_transaction_session_timeout — see Required alongside this PR.

Fix

Centralized in the three session entry points (get_db, get_read_db, tracked_db) — every subsystem (deriver, dreamer, reconciler, webhooks, streaming) opens sessions through these, so they all inherit the fix with no call-site changes.

  • _run_to_completion — runs cleanup as a detached task and waits on it via asyncio.wait (which never cancels the inner task), re-raising the cancellation only after cleanup finishes. Survives a cancel storm. A single monotonic deadline bounds the whole loop so a wedged connection can't pin the task.
  • _finalize_session — rollback then always close (inner try/finally); invalidate() the connection on a broken-mid-protocol InterfaceError/OperationalError/DBAPIError so a dead connection isn't returned to the pool; cleanup errors are caught+logged so they can't mask the original error.

Two guarantees stack: structural (close() runs even if rollback() is cut off) + shield (survives repeated re-delivery).

Why not anyio.CancelScope(shield=True)

The ticket proposed it, but it does not defer a native asyncio.Task.cancel() — only anyio-scoped cancellation — verified empirically under both asyncio.run() and anyio.run(). Starlette (ASGI) and the deriver's uvloop both cancel natively, so the anyio shield would be inert exactly where we need it. The stdlib approach works in both runtimes and adds no dependency.

Testing

tests/test_dependencies.py22 tests pass against a live Postgres (ruff clean, basedpyright 0 errors):

  • ROLLBACK+close on CancelledError; on cancel re-delivered during cleanup; under a cancel storm; broken-connection → invalidate(); DI-generator teardown.
  • request_context reset even on the cancelled path (no contextvar leak in reused deriver tasks).
  • Wedged-cleanup is abandoned past a timeout rather than pinning the task forever — including while cancellation is re-delivered at anyio's cadence.
  • Real-DB wire chaos test: a real in-flight write transaction cancelled mid-flight leaves no idle in transaction backend (asserted via pg_stat_activity).

tests/test_db_resilience.py — 11 pass, no regression.

Review pass (addressed)

An adversarial correctness review + a ticket-compliance audit were run. Fixes applied:

  • Bounded cleanup: shielded cleanup is uninterruptible by design, so a wedged connection (dead socket, no libpq timeout) could pin the task forever — now abandoned past _CLEANUP_TIMEOUT_SECONDS.
  • Monotonic deadline (review round 2): that timeout was passed to each asyncio.wait() call, so it re-armed on every re-delivered cancel and never elapsed — dead exactly on the anyio/BaseHTTPMiddleware path it was written for. Now one deadline is computed before the loop. The storm test also re-cancels at anyio's call_soon cadence; sparse cancels leave quiet ticks that let a per-wait timeout re-arm, which is why the old test passed either way.
  • Context reset moved to its own finally so a cancelled cleanup can't leak the task-scoped contextvar.
  • Guarded fut.cancelled()/fut.exception() so loop shutdown can't raise from the tail.

Still to verify before merge

  • HTTP-level chaos: cancel real search / chat-stream requests via client disconnect (harness chaos_http.py written, needs a running server). The committed real-DB test covers the wire level; this adds the full request path.
  • Prod watch: idle-in-transaction count ~0 over 24h post-deploy.

Required alongside this PR (not code — do these regardless)

The process-death class above is only reclaimed server-side, so the first item is a companion to this PR rather than optional follow-up:

  • Server-side backstop: ALTER ROLE postgres SET idle_in_transaction_session_timeout = '2min'; — one line, covers every class including the ones no application fix can reach.
  • Immediate remediation runbook (pg_terminate_backend of stuck backends).
  • Monitoring alert on the existing idle_in_txn sampler in lock_monitoring.sql (count > 10 or max age > 10min).

Follow-up (not in this PR)

  • Consider TCP keepalives / tcp_user_timeout on the psycopg connect_args in db.py, so a black-holed socket errors out (→ invalidate()) instead of relying on the abandon-timeout. Shared infra; out of this ticket's scope.

Part of DEV-1861 — leaving the ticket open until the server-side timeout is applied and the 24h prod watch confirms idle-in-transaction ~0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved database session cleanup when requests are cancelled.
    • Ensured transactions are rolled back and connections are closed reliably, including during repeated cancellation events.
    • Added connection invalidation when rollback encounters database errors.
    • Prevented stale request context and idle transactions after interrupted requests.
    • Added a timeout safeguard for cleanup operations that become unresponsive.
  • Tests

    • Added coverage for cancellation, rollback failures, cleanup timeouts, and transaction-state recovery.

Cancelled client-facing requests (search, dialectic chat/stream) could
abandon an open transaction: when a request task is cancelled, the
CancelledError can be re-delivered onto the ROLLBACK/close awaits in the
session-cleanup `finally`, so ROLLBACK never reaches Postgres and close()
never runs. Supavisor (transaction mode) does not reset the orphaned
backend on disconnect, leaving it `idle in transaction` indefinitely.
These zombies accrued ~5/hour, exhausting the connection pool and pinning
the xmin horizon.

Fix, centralized in the three session entry points (get_db, get_read_db,
tracked_db) so every subsystem inherits it:

- `_run_to_completion`: run cleanup as a detached task and await it through
  asyncio.shield in a loop, so a (re-delivered) cancellation cannot
  interrupt ROLLBACK/close. Survives a cancel storm.
- `_finalize_session`: rollback then always close (inner try/finally);
  invalidate the connection on a broken-mid-protocol InterfaceError/
  OperationalError/DBAPIError so a dead connection is not returned to the
  pool; never let a cleanup error mask the original.

Note: anyio.CancelScope(shield=True) — the originally-proposed primitive —
does NOT defer a native asyncio.Task.cancel() (verified empirically under
both asyncio and anyio loops), which is how Starlette and the deriver's
uvloop cancel; hence the stdlib approach. No new dependency.

Tests: cancellation, re-delivery-during-cleanup, cancel-storm,
broken-connection->invalidate, and DI-path teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Async database dependency teardown now uses bounded, cancellation-resistant session finalization, including rollback failure invalidation and guaranteed context reset. Tests cover cancellation, cleanup failures, timeout abandonment, and pooled PostgreSQL transaction state.

Changes

Database cleanup hardening

Layer / File(s) Summary
Cleanup primitives
src/dependencies.py
Adds shielded, bounded cleanup and centralized session rollback, invalidation, and close handling.
Dependency teardown wiring
src/dependencies.py
Routes get_db, get_read_db, and tracked_db through the finalization helpers and preserves request-context reset.
Cancellation and failure regression coverage
tests/test_dependencies.py
Expands session doubles and tests cancellation propagation, cleanup failures, timeout abandonment, context reset, and pooled-connection state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant DatabaseDependency
  participant CleanupRunner
  participant Session
  participant PostgreSQL
  Request->>DatabaseDependency: use database session
  Request-->>DatabaseDependency: cancellation or request failure
  DatabaseDependency->>CleanupRunner: run bounded finalization
  CleanupRunner->>Session: rollback
  Session->>PostgreSQL: release transaction state
  CleanupRunner->>Session: invalidate on rollback error
  CleanupRunner->>Session: close
  DatabaseDependency->>DatabaseDependency: reset request context
Loading

Suggested reviewers: rajat-ahuja1997

Poem

A rabbit hops through cancelled air,
And tidies sessions with careful care.
Rollback, close, then context too,
Even storms won’t break the queue.
The database rests, its work all done.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely matches the main change: making DB session cleanup resilient to task cancellation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

adavyas and others added 3 commits July 14, 2026 11:36
Keep the load-bearing "why" (the anyio-shield caveat; close-in-finally as
the backstop) and drop the restated Supavisor/idle-in-transaction prose that
the PR/ticket already carry. Also collapse the try/except-pass cleanup guards
to contextlib.suppress. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…el test

Review findings on the cancellation-shield fix:

- Bound _run_to_completion with _CLEANUP_TIMEOUT_SECONDS. Shielded cleanup is
  uninterruptible by design, so a wedged connection (dead socket, no libpq
  timeout) would otherwise pin the task forever; past the bound we abandon
  cleanup rather than hang. Switched the wait to asyncio.wait (shields fut,
  supports a timeout) and guarded fut.exception()/fut.cancelled() so loop
  shutdown can't raise from the tail.
- tracked_db: reset request_context in its own finally so a cancelled cleanup
  path can't leak the task-scoped contextvar into a reused long-lived task
  (deriver).
- Tests: reset-context-on-cancellation, abandon-wedged-cleanup, and a real-DB
  wire cancellation test asserting no `idle in transaction` backend lingers
  after a cancelled in-flight write transaction (pg_stat_activity).

Full suite: 21 passed live (incl. 4 pre-existing + 4 new real-DB tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adavyas
adavyas marked this pull request as ready for review July 14, 2026 19:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dependencies.py`:
- Around line 24-56: The _run_to_completion function must enforce one monotonic
overall cleanup deadline instead of resetting the timeout after each
cancellation. Compute a deadline before the wait loop, derive each asyncio.wait
timeout from the remaining time, and abandon cleanup once the deadline is
reached while preserving cancellation re-raising and existing task-error
handling.

In `@tests/test_dependencies.py`:
- Around line 554-566: Ensure the cancellation test’s backend cleanup always
runs when the final state assertion fails: wrap the retry and assertion logic
around backend_state(pid) in failure-safe cleanup that terminates the backend
identified by pid before propagating the assertion failure. Preserve the
existing successful cleanup verification and cancellation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15f55013-9c39-4579-a36b-718e007ec26f

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad2284 and 3bf7bd4.

📒 Files selected for processing (2)
  • src/dependencies.py
  • tests/test_dependencies.py

Comment thread src/dependencies.py
Comment on lines +24 to +56
async def _run_to_completion(coro: Coroutine[Any, Any, None]) -> None:
"""Run ``coro`` to completion even if the current task is cancelled (DEV-1861).

Runs the coro as a detached task and waits on it via ``asyncio.wait`` (which
never cancels the inner task), re-raising the cancellation only afterward.
Survives repeated re-delivery. NB: ``anyio.CancelScope(shield=True)`` does
NOT work here — it ignores a native ``asyncio.Task.cancel()``, which is how
Starlette and the deriver cancel.
"""
fut = asyncio.ensure_future(coro)
cancelled = False
while not fut.done():
try:
# wait() shields fut from our cancellation but still bounds the wait.
await asyncio.wait({fut}, timeout=_CLEANUP_TIMEOUT_SECONDS)
except asyncio.CancelledError:
cancelled = True # keep waiting so ROLLBACK/close reach Postgres
continue
if not fut.done():
# Wedged cleanup (dead socket). Stop pinning this task; leave fut to
# error out / be reclaimed rather than block indefinitely.
logger.error(
"DB session cleanup exceeded %.0fs; abandoning to avoid pinning "
"the task",
_CLEANUP_TIMEOUT_SECONDS,
)
break
if fut.cancelled():
cancelled = True # cleanup itself was cancelled (close() still ran)
elif fut.done() and (exc := fut.exception()) is not None:
logger.error("session cleanup task failed", exc_info=exc)
if cancelled:
raise asyncio.CancelledError()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== outline src/dependencies.py ==\n'
ast-grep outline src/dependencies.py --view expanded || true

printf '\n== outline tests/test_dependencies.py ==\n'
ast-grep outline tests/test_dependencies.py --view expanded || true

printf '\n== relevant lines src/dependencies.py ==\n'
sed -n '1,140p' src/dependencies.py | cat -n

printf '\n== relevant lines tests/test_dependencies.py ==\n'
sed -n '1,260p' tests/test_dependencies.py | cat -n

Repository: plastic-labs/honcho

Length of output: 20326


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "stormed_op|chaos_cancel|_run_to_completion|CLEANUP_TIMEOUT" src tests

Repository: plastic-labs/honcho

Length of output: 1435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '430,560p' tests/test_dependencies.py | cat -n

Repository: plastic-labs/honcho

Length of output: 6162


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '131,150p' tests/test_dependencies.py | cat -n

Repository: plastic-labs/honcho

Length of output: 992


Track a monotonic deadline across cancellation retries. asyncio.wait(..., timeout=_CLEANUP_TIMEOUT_SECONDS) resets on every CancelledError, so a cancellation storm can keep _run_to_completion() alive well past the intended 10s cap. The stormed_op/chaos_cancel tests exercise cleanup under repeated cancellation, but they don’t assert a hard wall-clock bound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dependencies.py` around lines 24 - 56, The _run_to_completion function
must enforce one monotonic overall cleanup deadline instead of resetting the
timeout after each cancellation. Compute a deadline before the wait loop, derive
each asyncio.wait timeout from the remaining time, and abandon cleanup once the
deadline is reached while preserving cancellation re-raising and existing
task-error handling.

Comment on lines +554 to +566
assert await backend_state(pid) == "idle in transaction" # precondition

task.cancel()
with pytest.raises(asyncio.CancelledError):
await task

state = await backend_state(pid)
for _ in range(20): # allow shielded cleanup a moment to land
if state != "idle in transaction":
break
await asyncio.sleep(0.1)
state = await backend_state(pid)
assert state != "idle in transaction", f"backend left {state!r} after cancel"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

On assertion failure, this test leaks a real orphaned backend connection.

If state never leaves idle in transaction within the 2s retry budget, the final assert fails but pid is never terminated — the backend stays parked exactly like the bug this PR fixes, potentially exhausting the shared pool for the rest of the test session. Terminate the backend as cleanup regardless of pass/fail.

🧹 Proposed fix to terminate the backend on failure
     state = await backend_state(pid)
     for _ in range(20):  # allow shielded cleanup a moment to land
         if state != "idle in transaction":
             break
         await asyncio.sleep(0.1)
         state = await backend_state(pid)
-    assert state != "idle in transaction", f"backend left {state!r} after cancel"
+    try:
+        assert state != "idle in transaction", f"backend left {state!r} after cancel"
+    finally:
+        if state == "idle in transaction":
+            async with read_engine.connect() as obs:
+                await obs.execute(text("SELECT pg_terminate_backend(:p)"), {"p": pid})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert await backend_state(pid) == "idle in transaction" # precondition
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
state = await backend_state(pid)
for _ in range(20): # allow shielded cleanup a moment to land
if state != "idle in transaction":
break
await asyncio.sleep(0.1)
state = await backend_state(pid)
assert state != "idle in transaction", f"backend left {state!r} after cancel"
assert await backend_state(pid) == "idle in transaction" # precondition
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
state = await backend_state(pid)
for _ in range(20): # allow shielded cleanup a moment to land
if state != "idle in transaction":
break
await asyncio.sleep(0.1)
state = await backend_state(pid)
try:
assert state != "idle in transaction", f"backend left {state!r} after cancel"
finally:
if state == "idle in transaction":
async with read_engine.connect() as obs:
await obs.execute(text("SELECT pg_terminate_backend(:p)"), {"p": pid})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_dependencies.py` around lines 554 - 566, Ensure the cancellation
test’s backend cleanup always runs when the final state assertion fails: wrap
the retry and assertion logic around backend_state(pid) in failure-safe cleanup
that terminates the backend identified by pid before propagating the assertion
failure. Preserve the existing successful cleanup verification and cancellation
behavior.

@ulyssepence ulyssepence left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read over the code in src/dependencies.py and it looks good to me. Interesting how asyncio.wait can dodge its coroutine being canceled!

The only thing that I wasn't clear on from the ticket is how confident we are that this is the problem. Was there something like a smoking gun or was this more a best guess?

@karaokedjodua

Copy link
Copy Markdown

Finn-loop review of 3bf7bd4

CI: not configured (no required checks reported on the branch)
Mergeability: BLOCKED (stale branch-protection rule — mergeable: MERGEABLE, no required checks)

Review

Summary: Hardens all three DB session entry points (get_db, get_read_db, tracked_db) against task cancellation by shielding the ROLLBACK+close cleanup path via _run_to_completion (detached task + asyncio.wait loop). An added _finalize_session helper ensures close() runs even when rollback() is interrupted, and invalidates connections that hit mid-protocol errors. The 10s abandon timeout prevents a wedged connection from pinning the task forever. Applies uniformly across the entire codebase with zero call-site changes.

1. Must fix before merge

[CI] No required CI is configured on this branch — gh pr checks 908 --required reports "no required checks reported on the 'adavya/dev-1861' branch". Merge state is BLOCKED with mergeable: MERGEABLE, indicating a stale branch-protection rule referencing a check that no longer exists or matches. An admin must update or remove the stale rule before this can merge. (This is a repo-configuration gap, not a code defect.)

2. Should fix soon

None — the code changes are clean and well-tested.

3. CI check details (not required)

  • test-python: FAIL (1m12s) — pre-existing failure in tests/crud/test_document.py::test_duplicate_rejection_reinforces_existing (ValueError: OpenAI API key is required), unrelated to this PR's changes (src/dependencies.py, tests/test_dependencies.py).
  • test-status: FAIL (3s) — implausibly short duration; likely a CI setup issue, not a code failure.
  • CodeRabbit: PASS
  • changes: PASS

4. Code review

Contract compliance ✓

Per the PR body (no formal Paperclip issue linked — Linear DEV-1861): the fix addresses the idle in transaction zombie-connection leak with the claimed approach. ACs from the PR body:

  • ✅ ROLLBACK+close runs under cancellation → _run_to_completion shields via asyncio.wait
  • ✅ Survives cancel re-delivery during cleanup → loop re-catches CancelledError
  • ✅ Invalidates broken connections → _finalize_session catches InterfaceError/OperationalError/DBAPIError
  • ✅ Contextvar reset survives cancel → nested finally in tracked_db
  • ✅ Abandon timeout for wedged cleanup → 10s _CLEANUP_TIMEOUT_SECONDS

Code quality

  • No anyio.CancelScope(shield=True) antipattern (empirically verified ineffective against native asyncio.Task.cancel())
  • _finalize_session inner finally for close() is the correct structural guarantee
  • No cross-repo fork dependency concerns — all new symbols defined within dependencies.py, all imports are stdlib or existing deps
  • The abandoned-cleanup path logs and leaks the detached task; acceptable for a dead-socket edge case (OS/pool reclaim)

Tests ✓

21 tests in test_dependencies.py (10 new for DEV-1861) plus 11 unchanged in test_db_resilience.py. Coverage includes: cancellation, re-delivery during rollback, cancel storm, broken-connection invalidation, contextvar reset, wedged-cleanup abandon timeout, and a real-DB pg_stat_activity assertion that no idle in transaction backend survives a cancelled write transaction.

5. Safe to merge

The code changes are correct, self-contained, and comprehensively tested. The merge is blocked by CI configuration (no required checks, stale branch-protection rule) — an admin needs to resolve that before merging.


Labels to apply (no admin rights on this token):

  • needs-human-review — no required CI configured + stale branch-protection rule
  • loop-approved — code review passes

@VVoruganti VVoruganti closed this Jul 24, 2026
@VVoruganti VVoruganti reopened this Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants