Skip to content

fix: an accepted cancel calls back before the query's HY008, done and free - #495

Merged
TimelordUK merged 1 commit into
masterfrom
fix/cancel-cb-before-hy008
Oct 22, 2026
Merged

TimelordUK merged 1 commit into
masterfrom
fix/cancel-cb-before-hy008

Conversation

@TimelordUK

Copy link
Copy Markdown
Owner

Problem

An accepted cancel ends a query with two native completions, which run on different threads:

  • the cancel call's own callback (cancel-cb ok);
  • the native call that was in flight, which comes back with HY008. JS then emits error, done and free.

Either one can reach JS first, so callers saw error HY008, and sometimes even done, before the cancel callback. On Linux CI this failed at least one contract scenario on most runs:

Fix (JS only)

  • lib/notifier.js: once a cancel has been sent, it is marked pending, and its callback is wrapped to settle it. Anything waiting on it is released one tick after the callback has run, so work the callback queues still goes first. For example, a second cancel's "already requested" refusal comes before the HY008. If cancelQuery throws, the cancel settles at once, so nothing is held for good.
  • lib/reader.js: whatever begin, dispatch and nextResult get back from native now goes through afterCancel. While a cancel is pending it waits for the cancel's callback; with no cancel pending it runs at once, as before.

The native CancelWorker calls back without waiting on JS: it either calls SQLCancelHandle or sets the poll loop's flag. So the hold cannot hang.

An accepted cancel is now always cancel-cb ok → error HY008 → done → free. The two corpus files that accepted either order (--- or) now accept only that order.

Testing (local, WSL Ubuntu 22.04, SQL Server 2022)

  • contract.test.js plus replay.test.js: 345 passing, on two full runs.
  • Live stress, capture.js --repeat 20 over cancel/, queue/ and scripts/issue-479 (33 scenarios × 20 runs):
    • with the fix: 0 differences;
    • unfixed lib/: the race hit once, on cancel/last-row (HY008 before cancel-cb ok).
  • explore.js (jittered replay over the fake driver) holds with the fix, but it also holds without it. The fake driver releases results in the order they were recorded, so it cannot produce the other order. It is not evidence either way.

The real test is CI's Linux jobs, where this failed on most runs.

🤖 Generated with Claude Code

… free

The cancel's own callback and the HY008 from the native call in flight are
two native completions on different threads, so either could reach JS
first: `error HY008` (and on #479's script even `done`) arrived before
`cancel-cb ok`. CI on Linux hit it on most runs, on
cancel/again-after-confirm-at-100, cancel/twice-streaming-at-100,
cancel/last-row and scripts/issue-479.

- notifier: a cancel that has been sent but not called back is pending. Its
  callback is wrapped to settle it, and what waits on it is released a tick
  on, after anything the callback queued (e.g. a second cancel's refusal).
  A cancelQuery that throws settles at once, so nothing is held for good.
- reader: what begin, dispatch and nextResult get back from native runs
  through afterCancel, so it waits for the cancel's callback. With no cancel
  pending it runs at once, as before.

The native cancel worker calls back without waiting on JS (it either calls
SQLCancelHandle or sets the poll loop's flag), so holding cannot hang.

The two corpus files that accepted either order (`--- or`) now accept only
cancel-cb, HY008, done, free.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@TimelordUK
TimelordUK merged commit c020ca4 into master Oct 22, 2026
11 checks passed
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