Skip to content

fix: Abort running R2DBC statement when transaction coroutine is cancelled - #2942

Open
Matthew de Detrich (mdedetrich) wants to merge 1 commit into
JetBrains:mainfrom
mdedetrich:fix/r2dbc-cancel-running-statement
Open

Matthew de Detrich (mdedetrich) wants to merge 1 commit into
JetBrains:mainfrom
mdedetrich:fix/r2dbc-cancel-running-statement

Conversation

@mdedetrich

@mdedetrich Matthew de Detrich (mdedetrich) commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

Summary of the change: Cancelling a coroutine running an R2DBC transaction now aborts the in-flight statement on the database (PostgreSQL), instead of waiting for it to run to completion.

Detailed description:

  • Why: R2DBC drivers drain a cancelled subscription rather than aborting the query. Exposed's rollback and connection release then queue behind the still-running statement, so a cancelled coroutine only completes once the query finishes (e.g. a pg_sleep(30) cancelled after 2s took ~28s to join). Exposed holds the executing connection internally but never used it to cancel.

    At work, I am doing batch jobs in long running transactions on Postgres and currently there isn't a way to cancel the underlying transaction if the batches get cancelled. It would be ideal to model this using coroutine cancellation so that if an Exposed transaction in a coroutine gets cancelled it also cancels the database transaction.

  • What:

    • Each R2dbcConnectionImpl tracks whether a statement may still be executing.
    • On CancellationException in a transaction block, a driver-specific cancel request is sent on the transaction's own connection before rollback.
    • New R2dbcDatabaseConfig.cancelRunningStatementOnCancellation (default true) to opt out.
  • How:

    • New internal InFlightStatementTracker wraps statement result publishers; a statement counts as in flight from subscription until a terminal signal (which never arrives after a downstream cancel).
    • Connections are unwrapped via io.r2dbc.spi.Wrapped (so pooled connections work) down to PostgresqlConnection, and cancelRequest() is called. The Postgres driver is compileOnly, so this is guarded by a classpath check; other drivers are a no-op.
    • The cancel is sent only when a statement is tracked as in flight and a connection has already been acquired (it never acquires one). It is awaited before rollback/release, so it cannot hit a later statement or the next pool borrower; a cancel that arrives after the statement finished lands on an idle backend and is ignored.

Type of Change

Please mark the relevant options with an "X":

  • Bug fix
  • New feature
  • Documentation update

Updates/remove existing public API methods:

  • Is breaking change

Affected databases:

  • MariaDB
  • Mysql5
  • Mysql8
  • Oracle
  • Postgres
  • Redshift
  • SqlServer
  • H2
  • SQLite

Checklist

  • Unit tests are in place
  • The build is green (including the Detekt check)
  • All public methods affected by my PR has up to date API docs
  • Documentation for my change is up to date

@mdedetrich
Matthew de Detrich (mdedetrich) force-pushed the fix/r2dbc-cancel-running-statement branch 2 times, most recently from 842782d to 18a068d Compare September 30, 2026 19:46

This branch has not been deployed

No deployments
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