Skip to content

Standardise deferred pgx Rollback() calls on a non-cancelable context - #420

Open
dpage wants to merge 3 commits into
mainfrom
fix/issue-381-rollback-context-sweep
Open

Standardise deferred pgx Rollback() calls on a non-cancelable context#420
dpage wants to merge 3 commits into
mainfrom
fix/issue-381-rollback-context-sweep

Conversation

@dpage

@dpage dpage commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

Deferred tx.Rollback() calls were inconsistent across the tree: two
sites deliberately passed context.Background(), each carrying its own
copy of the explanation, whilst the remaining twenty passed the
request-derived ctx. This sweeps every rollback in the server,
collector, and alerter onto a non-cancelable context, in one deliberate
pass.

The reason it matters is that pgx v5 treats a rollback on a cancelled
context as a failed rollback, and a failed rollback is unrecoverable:
conn.die() runs, so the pooled connection is discarded whilst its
transaction is still open on the server. A client that closes a tab or
times out mid-request therefore leaked a connection in an
aborted-transaction state, and could also trip the
close-of-closed-channel panic tracked as
jackc/pgx#2470.

Transaction semantics, commit paths, and error handling are untouched;
only the context handed to Rollback changes.

Sites converted (20)

  • server/src/internal/api/perf_summary_handlers.go: handlePerfSummary,
    handleDatabaseSummaries, handleTopQueries
  • server/src/internal/api/query_handlers.go: the deferred cleanup in
    executeQuery
  • server/src/internal/database/cluster_queries.go:
    DeleteAutoDetectedCluster, DismissAutoDetectedClusterKeys
  • server/src/internal/database/relationship_queries.go:
    SetNodeRelationships, SyncAutoDetectedRelationships,
    RemoveServerFromCluster
  • server/src/internal/database/alert_queries.go: AcknowledgeAlert,
    UnacknowledgeAlert
  • server/src/internal/tools/transaction.go: seven rollbacks across
    BeginReadOnlyTx and BeginTx (panic recovery, uncommitted cleanup,
    and setup failure)
  • alerter/src/internal/database/alert_queries.go: ReactivateAlert
  • collector/src/probes/storage.go: StoreMetrics
  • collector/src/database/schema.go: both migration error paths

server/src/internal/database/connection_queries.go (DeleteConnection)
was already hardened and keeps its behaviour, with the duplicated inline
rationale reduced to a pointer.
server/src/internal/auth/token_scope.go uses database/sql, whose
Rollback() takes no context, so it needs no change.

The issue listed 10 sites; the sweep also covers the deferred cleanup
closures and inline error-path rollbacks in query_handlers.go,
tools/transaction.go, probes/storage.go, and schema.go, because a
half-converted file cannot be enforced or explained coherently. The two
schema.go sites already ran on a background context in practice
(Migrate creates its own), so the change there makes the intent
explicit rather than altering behaviour.

Documentation

  • The rationale now lives once in
    .claude/golang-expert/transaction-rollback.md, listed in the
    golang-expert agent's knowledge base.
  • A short "Transaction Rollbacks" note sits under "Go Code" in
    docs/developer-guide/contributing.md, so new transactions get it
    right by default.
  • docs/changelog.md records the fix.

Test plan

  • server/src/internal/database/rollback_context_integration_test.go is
    the behavioural regression test. It drives UnacknowledgeAlert over a
    single-connection pool with a pgx QueryTracer that cancels the
    request context the moment the last statement completes, then asserts
    the ROLLBACK ran on an uncancelled context and that the pooled
    backend PID is unchanged. Verified to fail on the pre-fix code for
    both reasons: the rollback ran on the cancelled context, and the
    backend PID changed because pgx discarded the connection.
  • server/src/internal/database/rollback_convention_test.go parses every
    non-test Go file under server/src, collector/src, and
    alerter/src and rejects any single-argument Rollback call not given
    context.Background(). Verified to fail when one site is reverted.
  • server/src/internal/tools/transaction_rollback_integration_test.go
    covers all seven rollbacks in BeginReadOnlyTx/BeginTx, including
    the panic-recovery and setup-failure paths.
  • New handler tests drive the three performance-summary endpoints and the
    query executor end to end, following the harness pattern established in
    Fix two dashboard charts that plotted the wrong data #416: a real handler wired to Postgres over a trimmed metrics schema,
    driven through httptest. Helper and schema names are distinct from
    Fix two dashboard charts that plotted the wrong data #416's, and no production line that Fix two dashboard charts that plotted the wrong data #416 touches is modified here.
  • Reaching the remaining branches needed realistic failure injection
    rather than mocks: a query tracer that cancels the request context
    between statements, schema drift such as a dropped column, a migration
    that closes its own connection, and a monitored server on a dead port.

Coverage of the touched units

Every function whose rollback changed now clears the 90% floor:

Function Before After
handlePerfSummary 0.0% 94.5%
handleDatabaseSummaries 0.0% 90.0%
handleTopQueries 0.0% 95.9%
executeQuery 25.0% 91.7%
BeginReadOnlyTx 50.0% 100%
BeginTx 52.9% 100%
AcknowledgeAlert 0.0% 92.3%
UnacknowledgeAlert 81.0% 90.5%
SetNodeRelationships 0.0% 92.9%
RemoveServerFromCluster 0.0% 95.2%
SyncAutoDetectedRelationships 95.0% 95.0%
DeleteConnection 94.7% 94.7%
DeleteAutoDetectedCluster 76.0% 92.0%
DismissAutoDetectedClusterKeys 79.2% 91.7%
ReactivateAlert (alerter) 78.6% 92.9%
StoreMetrics (collector) 92.5% 92.5%
Migrate (collector) 72.7% 90.9%

Of the 22 modified lines, 21 are executed by tests, verified block by
block against the coverage profiles.

One uncoverable line, and why

The rollback in StoreMetrics
(collector/src/probes/storage.go) cannot be reached by any test. Its
deferred guard tests the outer err from conn.Begin, but both the
INSERT and Commit failure paths declare their own err with :=, so the
outer variable is never assigned and the guard is never true:

if _, err := txn.Exec(ctx, query, args...); err != nil {
    return fmt.Errorf("failed to execute INSERT: %w", err)
}

The existing TestStoreMetrics_ErrorPath provokes exactly that failure
and the rollback still does not run. This is a pre-existing defect rather
than something the sweep introduced, and fixing it would change
transaction behaviour, so it is left alone here and is worth its own
issue.

Verification

  • gofmt clean and golangci-lint run reports 0 issues in all three Go
    modules. Note that misspell enforces the US locale in Go sources, so
    the new tests use "canceled" whilst the prose keeps British spelling.
  • Full make coverage for the server, collector, and alerter run against
    a local Postgres. The only failures are the two pre-existing pgvector
    fixture failures in internal/tools
    (TestStoreMemoryGeneratesEmbeddingIntegration,
    TestRecallMemoriesGeneratesQueryEmbeddingIntegration), confirmed to
    fail identically on main and already addressed by fix(server): widen memory embedding test fixture to halfvec(4000) #382. They skip in
    CI, which has no pgvector.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N44XgdgR2msyNydAao5vXY

The pgx v5 driver treats a rollback issued on a cancelled context as a
failed rollback, and a failed rollback is unrecoverable: pgx calls
conn.die(), so the pooled connection is discarded whilst its transaction
is still open on the server. A client that closes a tab or times out
mid-request therefore leaked a connection in an aborted-transaction
state, and could also trip the close-of-closed-channel panic tracked as
jackc/pgx#2470.

Two call sites already guarded against this, each carrying its own copy
of the explanation, whilst the remaining twenty passed the
request-derived context. This sweeps every rollback in the server,
collector, and alerter onto context.Background(), whether deferred
directly, deferred inside a cleanup closure, or issued inline on an
error path. Transaction semantics, commit paths, and error handling are
untouched; only the context handed to Rollback changes.

The rationale now lives once in .claude/golang-expert/
transaction-rollback.md, with a short note under "Go Code" in the
contributor guide so new transactions get it right by default, and the
duplicated inline explanations are reduced to a pointer.

Two tests lock the behaviour in. A regression test drives
UnacknowledgeAlert with a pgx QueryTracer that cancels the request
context the moment the last statement completes, then asserts the
ROLLBACK ran on an uncancelled context and that the pooled backend PID
is unchanged; it fails on the pre-fix code for both reasons. A
convention test parses every non-test Go file in all three modules and
rejects any single-argument Rollback call that is not given
context.Background(). Coverage of the touched transaction paths is
raised alongside, including the previously untested AcknowledgeAlert,
SetNodeRelationships, and RemoveServerFromCluster.

Closes #381
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 28 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0fe11249-da38-41fb-b6dc-e6d1b734828c

📥 Commits

Reviewing files that changed from the base of the PR and between 19c645d and a2cf840.

📒 Files selected for processing (24)
  • .claude/agents/golang-expert.md
  • .claude/golang-expert/transaction-rollback.md
  • alerter/src/internal/database/alert_queries.go
  • alerter/src/internal/database/reactivate_alert_error_paths_test.go
  • collector/src/database/schema.go
  • collector/src/database/schema_migrate_error_paths_test.go
  • collector/src/probes/storage.go
  • docs/changelog.md
  • docs/developer-guide/contributing.md
  • server/src/internal/api/perf_summary_endpoints_rollback_test.go
  • server/src/internal/api/perf_summary_handlers.go
  • server/src/internal/api/query_execute_rollback_test.go
  • server/src/internal/api/query_handlers.go
  • server/src/internal/database/acknowledge_alert_integration_test.go
  • server/src/internal/database/alert_queries.go
  • server/src/internal/database/cluster_queries.go
  • server/src/internal/database/connection_queries.go
  • server/src/internal/database/relationship_queries.go
  • server/src/internal/database/rollback_context_integration_test.go
  • server/src/internal/database/rollback_convention_test.go
  • server/src/internal/database/rollback_sweep_error_paths_test.go
  • server/src/internal/database/set_node_relationships_integration_test.go
  • server/src/internal/tools/transaction.go
  • server/src/internal/tools/transaction_rollback_integration_test.go

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

@codacy-production

codacy-production Bot commented Aug 12, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 458 complexity · 176 duplication

Metric Results
Complexity 458
Duplication 176

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@dpage

dpage commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

dpage added 2 commits August 12, 2026 15:43
Every function whose rollback the sweep converted now clears the
project's 90% line-coverage floor, and every modified line but one is
executed by a test.

The three performance-summary endpoints and the query executor were the
gap: their handlers had no test at all, so the converted rollback lines
were never reached. The new handler tests follow the harness pattern
established in PR #416, wiring a real handler to the local Postgres over
a trimmed metrics schema and driving it through httptest. Distinct helper
and schema names keep the two test files independent, and no production
line #416 touches is modified here.

Coverage of the touched units, before and after:

  handlePerfSummary                0.0% -> 94.5%
  handleDatabaseSummaries          0.0% -> 90.0%
  handleTopQueries                 0.0% -> 95.9%
  executeQuery                    25.0% -> 91.7%
  UnacknowledgeAlert              81.0% -> 90.5%
  DeleteAutoDetectedCluster       76.0% -> 92.0%
  RemoveServerFromCluster          0.0% -> 95.2%
  Migrate (collector)             72.7% -> 90.9%

Reaching the last few branches needed realistic failure injection rather
than mocks: a query tracer that cancels the request context between
statements, schema drift such as a dropped column, a migration that
closes its own connection, and a monitored server on a dead port.

The one modified line that remains uncovered is the rollback inside
StoreMetrics in collector/src/probes/storage.go. Its guard tests an
outer err that the INSERT and Commit failure paths shadow with :=, so
the guard can never be true and the rollback is unreachable. That is a
pre-existing defect worth its own issue; this change deliberately does
not alter the behaviour.
The new executeQuery tests seeded a connection row with no password,
which works against a loopback server using trust authentication but
fails on CI, where Postgres authenticates. The fixture now encrypts the
test connection string's password with a fixed test secret and stores it
the way the production create path does, so the handler also exercises
the decrypt step. A target with no password still stores NULL.
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.

1 participant