You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: stop msnodesqlv8 streams resumed after they end from hanging the connection - #1921
This builds on the msnodesqlv8 stream-completion work, which has no PR of its own. The first two commits are that work:
test: read the test config afresh for each test
fix: complete msnodesqlv8 streams when an error ends the batch
Please review the last commit:fix: stop msnodesqlv8 streams resumed after they end from hanging the connection.
The branch is rebased onto master, which now includes #1918 and #1919, and the fix was rebuilt on #1919's design. The stream-completion commit also runs #1918's query with pipe and an error tests on every msnodesqlv8 version. They had been skipped because a stream never completed after an error.
Problem
msnodesqlv8 does not check that a query is still running when it is paused, resumed or cancelled. Its work queue implements park/resume as remove + add, so pausing and then resuming a query that has ended, or resuming one that ended while it was paused, puts the dead work item back at the head of the connection's queue. The next query on that pooled connection never runs. From 5.1.9, resume() also re-runs the query's completion: it loops on rowcount events, releases the statement twice, and has segfaulted the process.
#1919 already stops a stream from pausing or resuming its query once msnodesqlv8 reports 'done'. The stream-completion commit makes 'free' do the same. Before that report, the request could still reach an ended query:
while the errors that end a batch are emitted, which msnodesqlv8 >= 5.1 does before it releases the statement;
between such an error and 'free'. A stream paused before its first result set (as toReadableStream() is) and read on after the error is one example;
in the few ticks between the query's reader stopping and msnodesqlv8 reporting the end;
on a cancel in any of those windows.
Fix
The request leaves the query alone once msnodesqlv8 reports that it has ended. That means 'done', 'free', an error with more=false, or msnodesqlv8 2.0's statement-terminated message. Nothing it emits from then on can reach the query.
It takes the query up again if msnodesqlv8 carries on to a result set after an error it said had nothing more to follow.
A cancel made once the query looks to have ended is held. It is sent if msnodesqlv8 carries on to a result set after all. Once the query is through, a paused stream completes as cancelled without waiting to be resumed, and anything else completes normally.
While an error that is not the last of its set is handled, the query is set aside. A cancel made then waits until the error has been handled. Afterwards the query is paused if the stream was paused before its first result set, unless it is being cancelled.
From msnodesqlv8 3, the request leaves the query alone once its reader has stopped.
Evidence
All live runs used Windows CI, SQL Server 2022 and msnodesqlv8 2.7.0, 3.1.0, 4.5.0, 5.2.1 and 5.6.0, from a throwaway branch.
Resuming after a stream ended hung the next query on every version, and looped or segfaulted on 5.2.1/5.6.0.
The fixed adapter passed all 20 probe scenarios on all five versions.
On current master:
Error-path scenarios: master hangs in every one, and the stream-completion commit fixes all of them.
The other scenarios: master + stream completion and the fixed adapter both pass every one tried. That covers 23 scenarios, plus a 0–16 ms resume-timing sweep on a stream paused before it runs.
Where the fix adds value: it stops pause, resume and cancel from reaching an ended query in the windows above. Live, the parent sent two driver calls to an ended query when a paused stream was cancelled after its error (3.1–5.6), and the fix sent none.
Where the windows are only reproduced off the live server: the rest are reproduced with msnodesqlv8's real JS over a fake native layer, and are pinned by unit tests.
Tests:
Unit: the new tests in the msnodesqlv8 query events block each fail without the guard they target. All 17 mutations of the guards fail at least one, and npm run test-unit passes (215).
Integration:pausing and resuming a stream once it is over (completed, failed, cancelled), wired for both drivers. On today's master it is a regression test: fix: stop msnodesqlv8 streams losing rows under back pressure #1919 together with the stream-completion commit already pass it.
Local:npm run lint. npm run test-tedious has 175 passing; the 3 failures are CLR tests that fail on Azure SQL Edge and also fail on master.
Known, unchanged
On msnodesqlv8 5.6, resuming a stream that was paused before it ran while its query is still executing can add a spurious HY010 Function sequence error. The next query still runs. This happens the same way with or without this PR.
readConfig() returned the object that require() caches, so a test that
changed its config changed it for every test after it. The login failed
test sets a user that does not exist, which leaves any later test that
opens its own pool from readConfig() unable to log in, and the request
timeout tests leave a 1 second timeout behind. Each call now parses the
file again, so a test's changes stay with that test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve cancellation requested while query is provisionally detached
lib/msnodesqlv8/request.js:675
Reattaching the query here drops a cancellation made from this error's listener. While detached is true, cancel() returns without recording anything for an unpaused stream; if meta then proves that the driver carried on after the more=false error, the remaining statements execute despite the caller's request.cancel(). Preserve a cancel requested during this provisional-detach window and replay it when metadata makes the query reachable again, while retaining the no-op behavior when free/done confirms that it really ended.
msnodesqlv8 emits 'done' when a batch completes, but not when an error
ends it: it emits the error, with no more to follow, then 'free' once it
has released the statement. Every version from 2.x to 5.6 does this. A
request that is not streamed settles on that error. A streamed request
emits each error as it arrives and only completed on 'done', so after a
batch-ending error (a failed statement, a conversion error, or a request
timeout) it never emitted 'done', never called back or settled its
promise, and never released its connection. Enough of them would leave
the pool with no connections to lend and close() would never resolve.
The request now completes on whichever of 'done' and 'free' comes first;
msnodesqlv8's own query aggregator settles a query on 'free' too. A
stream emits the error and then 'done', as it does with tedious, and the
connection is released once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… connection
msnodesqlv8 does not check that a query is still running when it is
paused, resumed or cancelled. Pausing and then resuming a query that has
ended, or resuming one that ended while it was paused, puts it back at
the head of its connection's work queue, where it holds up every query
the connection is sent after it, so a pooled connection hangs on the
next request it is lent to. From msnodesqlv8 5.1.9 resuming such a query
also has it ask for its next result again and release its statement a
second time, which can loop emitting rowcount events or crash the
process.
A stream stops pausing and resuming its query once msnodesqlv8 reports
'done' or 'free', but until then it still could after the query had
ended: while the errors that end its batch were emitted, which
msnodesqlv8 >= 5.1 does before it releases the statement; between such
an error and 'free', as a stream paused before its first result set is
when it is read on after the error; and in the few ticks between the
query's reader stopping and msnodesqlv8 reporting the end. A cancel in
those windows reached the ended query too.
The request now leaves the query alone once msnodesqlv8 reports that it
has ended, on an error that ends the batch as well as on 'done' and
'free', and takes it up again if msnodesqlv8 goes on to a result set
after an error it said had nothing more to follow. A paused stream that
is cancelled then still completes as cancelled. While an error that is
not the last of its set is handled the query is set aside, and it is
paused afterwards if the stream was paused before its first result set,
unless it is being cancelled. From msnodesqlv8 3 the request also leaves
the query alone once its reader has stopped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PR
This builds on the msnodesqlv8 stream-completion work, which has no PR of its own. The first two commits are that work:
test: read the test config afresh for each testfix: complete msnodesqlv8 streams when an error ends the batchPlease review the last commit:
fix: stop msnodesqlv8 streams resumed after they end from hanging the connection.The branch is rebased onto master, which now includes #1918 and #1919, and the fix was rebuilt on #1919's design. The stream-completion commit also runs #1918's
query with pipe and an errortests on every msnodesqlv8 version. They had been skipped because a stream never completed after an error.Problem
msnodesqlv8 does not check that a query is still running when it is paused, resumed or cancelled. Its work queue implements park/resume as remove + add, so pausing and then resuming a query that has ended, or resuming one that ended while it was paused, puts the dead work item back at the head of the connection's queue. The next query on that pooled connection never runs. From 5.1.9,
resume()also re-runs the query's completion: it loops onrowcountevents, releases the statement twice, and has segfaulted the process.#1919 already stops a stream from pausing or resuming its query once msnodesqlv8 reports
'done'. The stream-completion commit makes'free'do the same. Before that report, the request could still reach an ended query:'free'. A stream paused before its first result set (astoReadableStream()is) and read on after the error is one example;Fix
'done','free', an error withmore=false, or msnodesqlv8 2.0's statement-terminated message. Nothing it emits from then on can reach the query.Evidence
All live runs used Windows CI, SQL Server 2022 and msnodesqlv8 2.7.0, 3.1.0, 4.5.0, 5.2.1 and 5.6.0, from a throwaway branch.
Before #1919, on the old parent:
On current master:
Tests:
msnodesqlv8 query eventsblock each fail without the guard they target. All 17 mutations of the guards fail at least one, andnpm run test-unitpasses (215).pausing and resuming a stream once it is over(completed, failed, cancelled), wired for both drivers. On today's master it is a regression test: fix: stop msnodesqlv8 streams losing rows under back pressure #1919 together with the stream-completion commit already pass it.npm run lint.npm run test-tedioushas 175 passing; the 3 failures are CLR tests that fail on Azure SQL Edge and also fail on master.Known, unchanged
HY010 Function sequence error. The next query still runs. This happens the same way with or without this PR.🤖 Generated with Claude Code