Skip to content

feat(crdb): experimental crdb query cancellation#3176

Draft
ecordell wants to merge 10 commits into
authzed:mainfrom
ecordell:crdb-inband-cancel
Draft

feat(crdb): experimental crdb query cancellation#3176
ecordell wants to merge 10 commits into
authzed:mainfrom
ecordell:crdb-inband-cancel

Conversation

@ecordell

@ecordell ecordell commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

When a request context is canceled mid-query on the CRDB datastore, pgx's default behavior is to destroy the connection. Under load this drains the write pool and triggers the metastable death spiral described in #2576. A previous attempt to fix this using pgx's built-in CancelRequestContextWatcherHandler (pgwire cancel protocol) was reverted in #2434 because CRDB applies pgwire cancels asynchronously; a late-arriving cancel could kill the next query on the connection.

This PR implements cancellation using CockroachDB's CANCEL QUERIES statement instead. A new pool.Canceler owns a small dedicated connection pool and a registry mapping each pooled write connection to its CRDB session_id (captured via SHOW session_id at connect). A custom ctxwatch.Handler fires on context cancellation and issues CANCEL QUERIES IF EXISTS (SELECT query_id FROM [SHOW CLUSTER STATEMENTS] WHERE session_id = '...') on a sibling connection, then blocks until that statement completes before releasing the original connection back to the pool. This sequencing eliminates the wrong-query race by construction: no cancellation can be in flight when the next query starts on the same session.

The feature is gated behind --datastore-experimental-crdb-query-cancellation (default off). When enabled, set --write-conn-acquisition-timeout=0; with connections no longer being destroyed on cancel, pool exhaustion under load is far less likely and the acquisition-timeout backpressure is no longer needed.

@github-actions github-actions Bot added area/cli Affects the command line area/datastore Affects the storage system area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) labels Jun 12, 2026
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.05263% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/datastore/crdb/pool/canceler.go 88.61% 6 Missing and 3 partials ⚠️
internal/datastore/crdb/crdb.go 52.95% 6 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@ecordell ecordell force-pushed the crdb-inband-cancel branch from 0d2ddf3 to 442de98 Compare June 15, 2026 14:46
@tstirrat15

Copy link
Copy Markdown
Contributor

When enabled, set --write-conn-acquisition-timeout=0; with connections no longer being destroyed on cancel, pool exhaustion under load is far less likely and the acquisition-timeout backpressure is no longer needed.

If the fallback behavior when the cancelpool isn't available is the old connection cancellation behavior, couldn't you still hit the degenerate condition?

@tstirrat15 tstirrat15 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.

See comments, otherwise LGTM

// HandleCancel implements ctxwatch.Handler.
func (h *CancelQueryContextWatcherHandler) HandleCancel(_ context.Context) {
h.cancelFinished = make(chan struct{})
h.cancelSucceeded.Store(false)

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.

Is the idea here that there are potentially multiple requests using this same handler? I'm wondering why this isn't in the constructor

go func() {
defer close(h.cancelFinished)
if err := h.canceler.CancelSessionQueries(h.pgConn); err != nil {
log.Warn().Err(err).Msg("in-band query cancellation failed; connection will be discarded")

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.

At the risk of Yet Another Metric, successful and failed cancellations seem like they'd be good things to count

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.

Nvm I see you beat me to it

Comment on lines +161 to +168
config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
if afterConnect != nil {
if err := afterConnect(ctx, conn); err != nil {
return err
}
}
return c.RegisterSession(ctx, conn)
}

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.

It feels like there ought to be a nicer way to do this but I know that's not on this code.

Comment on lines +178 to +180
config.ConnConfig.BuildContextWatcherHandler = func(pgConn *pgconn.PgConn) ctxwatch.Handler {
return NewCancelQueryContextWatcherHandler(pgConn, c, defaultCancelDeadlineDelay)
}

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.

Do we need to do the same wrapping behavior here?

func beginFuncExec(ctx context.Context, tx pgx.Tx, fn func(pgx.Tx) error) (err error) {
defer func() {
rollbackErr := tx.Rollback(ctx)
rollbackErr := tx.Rollback(context.WithoutCancel(ctx))

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.

Was this a latent bug or is this necessitated by the new approach?

@ecordell ecordell Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

latent bug! in cases where we would need to rollback due to a context cancel the rollback would always fail too.

@ecordell ecordell changed the title feat(crdb): experimental in-band query cancellation feat(crdb): experimental crdb query cancellation Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli Affects the command line area/datastore Affects the storage system area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants