Skip to content

feat: cancel the server-side statement on a host cancel notification (#126) - #127

Merged
aesslinger merged 2 commits into
mainfrom
feat/126-cancel-server-side-statement
Sep 30, 2026
Merged

aesslinger merged 2 commits into
mainfrom
feat/126-cancel-server-side-statement

Conversation

@aesslinger

@aesslinger aesslinger commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

Implements the plugin-side half of #126 — cancel the server-side statement when the host sends a cancel notification. Companion to TabularisDB/tabularis#832 (still OPEN), which will ship the host half: sending a fire-and-forget cancel notification when its plugin-call timeout fires. Today a timed-out execute_query abandons the JSON-RPC slot but leaves the Postgres backend running the statement — an orphaned query holding locks. This PR makes the plugin stop it.

Design (agreed in the tabularis#832 thread — host owns when, plugin owns how): each in-flight execute_query/execute_query_batch/explain_query registers a tokio_postgres::CancelToken keyed by request id, and on a cancel notification for a known id, sends pg_cancel_backend via a fresh matching-TLS connection. The in-flight query then resolves through its normal Err path ("canceling statement due to user request") as a normal error response for the original call. An unknown id (already finished, or arrived after cleanup) is a no-op, never an error.

Changes

  • src/cancel.rs (new) — CancelGuard RAII registry. register builds a boxed cancel action from the live CancelToken + ConnectionParams; drop deregisters on every return path (Ok or Err) so the map can't leak. cancel(id) runs the one-shot action. run_cancel picks NoTls vs make_tls_connect via needs_tls, matching the pool's two-branch shape; errors are logged and swallowed (fire-and-forget).
  • src/handlers/query.rs — cancel handler; CancelGuard threaded into run_batch_in_session (session + non-session paths) and exec_query.
  • src/rpc.rs — "cancel" dispatch; handle_line returns Option<Value>, None for a notification (no top-level id field) so no response is written. Notification-ness is decided by absence of the id field, not by method (id: null is a request whose id is null, not a notification).
  • src/main.rs — run_worker skips the stdout write for None — a stray response line for a notification would corrupt the protocol stream.
  • src/client.rs — pub(crate) make_tls_connect + pub(crate) needs_tls. Also fixes a real bug the helper's doc had: make_tls_connect always builds a TLS connector with no NoTls fallback, so callers must check needs_tls first (the pool already does this two-branch shape); cancel::run_cancel does the same.
  • src/bin/test_plugin.rs — handles the new Option<Value> return.
  • Tests — 5 cancel-registry unit tests (src/cancel_tests.rs, fake boxed action — a real CancelToken needs a live connection) + an #[ignore]-gated live test that sends pg_sleep(30), cancels it, and confirms via pg_stat_activity the backend stops.

Envelope

Per the issue's proposed shape (tabularis#832 hasn't fixed it yet): {"method":"cancel","params":{"id":<request-id>}} as a true id-less notification. If the host settles on a different shape, only rpc.rs's dispatch string + the cancel handler's param read change — the registry and the query-handler threading are envelope-agnostic.

Backward compatibility & dormancy

No host sends cancel today (verified: no cancel wiring on upstream/main). The new dispatch is never hit; CancelGuard register/drop is two cheap HashMap ops per query with no output change — byte-identical responses for every existing RPC. Dormant until tabularis#832 ships; safe to merge now (zero breakage) and lights up when #832 lands.

Ahead of the builtin for this one behavior (the builtin's cancel_query_impl only aborts its local tokio task, never calls pg_cancel_backend) — flagged per the repo's "don't improve on the builtin silently" rule, but scoped to a bug the reporter explicitly asked to fix.

Verification

  • cargo build --release
  • Cross-repo 83-test byte-for-byte parity suite (POSTGRES_PLUGIN_BIN against tabularis' src-tauri/tests/postgres_integration parity*): 83/83 pass — no existing behavior regressed.
  • 361 unit tests pass (5 new cancel-registry).
  • 30 live-DB tests pass (2 ignored: the new cancel test + the pgvector one; run the cancel test with --include-ignored).
  • cargo clippy --all-targets -- -D warnings; cargo fmt --all --check; npx markdownlint CHANGELOG.md — all clean.

Closes #126.

…126)

When a host plugin-call timeout fires today, the host abandons its pending
JSON-RPC slot but the Postgres backend keeps running the statement
server-side — an orphaned query holding locks. tabularis#832 will fix the
host half by sending a fire-and-forget cancel notification; this is the
plugin-side implementation, ready to wire up the moment that lands. Until
then a cancel never arrives and this is a safe no-op addition — no existing
RPC's behavior or response shape changes.

Design (agreed in the tabularis#832 thread: host owns *when*, plugin owns
*how*): each in-flight execute_query/execute_query_batch/explain_query
registers a tokio_postgres::CancelToken keyed by request id before running
the statement, and deregisters it when the query resolves. On a cancel
notification for a known id, CancelToken::cancel_query opens a fresh
matching-TLS connection and sends pg_cancel_backend; the in-flight query
future then resolves via its normal Err path ("canceling statement due to
user request") and flows out as a normal error response for the original
call. An unknown id (already finished, or arrived after cleanup) is a
no-op, never an error, so a late/duplicate cancel can't disrupt a later
query that reused the id.

- src/cancel.rs (new): a CancelGuard RAII registry. register builds a
  boxed cancel action from the live CancelToken + ConnectionParams; drop
  deregisters on every return path (Ok or Err) so the map can't leak.
  cancel(id) runs the one-shot action; run_cancel picks NoTls vs
  make_tls_connect via needs_tls, matching the pool's two-branch shape.
- src/handlers/query.rs: cancel handler; CancelGuard threaded into
  run_batch_in_session (session + non-session paths) and exec_query.
- src/rpc.rs: "cancel" dispatch; handle_line returns Option<Value>, None
  for a notification (no top-level id field) so no response is written.
  Notification-ness is decided by absence of the id field, not by method
  (id: null is a request whose id is null, not a notification).
- src/main.rs: run_worker skips the stdout write for None — a stray
  response line for a notification would corrupt the protocol stream.
- src/client.rs: pub(crate) make_tls_connect + needs_tls. Fixed a real
  bug the helper's doc had claimed: it always builds a TLS connector via
  build_tls_connector with no NoTls fallback, so callers must check
  needs_tls first and use NoTls directly when false (the pool already
  does this two-branch shape). cancel::run_cancel does the same.

Envelope (per the issue's proposed shape, since tabularis#832 hasn't fixed
it yet): {"method":"cancel","params":{"id":<request-id>}} as a true
id-less notification. If the host settles on a different shape, only
rpc.rs's dispatch string + the cancel handler's param read change — the
registry and the query-handler threading are envelope-agnostic.

TDD: 5 unit tests for the registry's bookkeeping (register/cancel/remove,
one-shot action, unknown-id no-op, CancelGuard drop-deregisters, no-id
guard is a no-op) using a fake boxed action — a real CancelToken needs a
live connection. A #[ignore]-gated live-database test sends pg_sleep(30),
cancels it, and confirms via pg_stat_activity that the backend stops;
run with --include-ignored.

Verified: cargo build --release; 361 unit + 30 live-DB (2 ignored: the
new cancel test + the pgvector one) against the local pg-tabularis-test
podman container; the cross-repo 83-test byte-for-byte parity suite
(POSTGRES_PLUGIN_BIN against tabularis' src-tauri/tests/postgres_integration
parity*) passes 83/83; cargo clippy --all-targets -D warnings; cargo fmt
--all --check; npx markdownlint CHANGELOG.md — all clean.

Dormant until tabularis#832 ships. Ahead of the builtin for this one
behavior (the builtin's cancel_query_impl only aborts its local tokio
task, never calls pg_cancel_backend) — flagged per the repo's "don't
improve on the builtin silently" rule, but scoped to a bug the reporter
explicitly asked to fix.
@aesslinger aesslinger added enhancement New feature or request prerelease:rc Version suggestion targets a release candidate labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown

Version suggestion

Based on this PR's title (feat) and the prerelease:rc label:

Current 1.0.0-rc.5
Suggested next tag v1.0.0-rc.6

This is informational only — no tag or release is created automatically yet.

1 similar comment
@github-actions

Copy link
Copy Markdown

Version suggestion

Based on this PR's title (feat) and the prerelease:rc label:

Current 1.0.0-rc.5
Suggested next tag v1.0.0-rc.6

This is informational only — no tag or release is created automatically yet.

@aesslinger aesslinger self-assigned this Sep 30, 2026
…r-side-statement

# Conflicts:
#	CHANGELOG.md
@aesslinger
aesslinger merged commit b78b0cf into main Sep 30, 2026
7 checks passed
@aesslinger
aesslinger deleted the feat/126-cancel-server-side-statement branch September 30, 2026 12:58
aesslinger added a commit that referenced this pull request Sep 30, 2026
Ships the two PRs merged since 1.0.0-rc.5: the NULL column-default filter
parity fix (#125, #122 — case-insensitive to match the builtin) and the
cancel server-side statement notification handler (#127, #126 — dormant
until tabularis#832 ships the host-side notification).

Verified: cargo build --release; 366 unit + 30 live-DB against the local
pg-tabularis-test podman container; the cross-repo 83-test byte-for-byte
parity suite (POSTGRES_PLUGIN_BIN against tabularis'
src-tauri/tests/postgres_integration parity*) passes 83/83; cargo
clippy --all-targets -D warnings; cargo fmt --all --check; npx
markdownlint CHANGELOG.md — all clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request prerelease:rc Version suggestion targets a release candidate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cancel the server-side statement when a host call-timeout notification arrives

1 participant