Repository navigation
Conversation
…sult set is cancelled A stream paused before its query reaches a result set, as toReadableStream() leaves one until it is read, holds msnodesqlv8's query paused. The query still runs the statements ahead of that result set, which have none, and msnodesqlv8 cancels a paused query by freeing its statement. Once the server has sent the row count of one of those statements, msnodesqlv8 is reading on to the next result when that free lands, which can crash the process. It did so in nearly every run with each msnodesqlv8 version tried, 2.7 to 5.6.0. A held query is now cancelled once it stops at its first result set, where it has nothing under way, and one that ends first completes as cancelled. Such a cancel no longer cuts short the statements ahead of that result set: it takes effect when they end, or when the request times out. With no request timeout, nothing limits the wait. msnodesqlv8 6 fixes the crash itself, and emits nothing for a paused query until it is resumed, so there a held query is cancelled at once, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
msnodesqlv8 6 requires Node 22 or later, and ships a Node-API binary for win32-x64, so the Windows jobs run its tests on Node 22 and 24 after the v5 run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
msnodesqlv8 6 takes one cancel per query. It refuses any other in its callback, while the one it took goes on to end the query. The request took a refused cancel to mean that the query was running on, as it is with earlier versions. So a second cancel() made before msnodesqlv8 had the query's statement undid the first one. The request then failed with 'Operation canceled' rather than ECANCEL, and a paused stream never completed. With msnodesqlv8 6 the request now sends one cancel per query, and the outcome of that one stands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 tasks
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What goes wrong
msnodesqlv8 6 takes one cancel per query. It refuses any other in its callback (
cannot cancel query (cancel already requested)), while the one it took goes on to end the query:HY008, thendone, thenfree.The adapter treats a refused cancel as a query that runs on, which is true before 6. A cancel refused by msnodesqlv8 < 5.6 means the query keeps running. So when
cancel()is called twice before msnodesqlv8 has the query's statement, the second cancel's refusal undoes the first:HY008is no longer dropped. The request is rejected withEREQUEST"Operation canceled" instead ofECANCEL.The window is short, but calling
cancel()twice in a row is enough to reach it.The fix
With msnodesqlv8 6, the request sends msnodesqlv8 one cancel per query, and the outcome of that one stands. A later
cancel()does nothing: msnodesqlv8 would refuse it anyway.Earlier versions are unchanged. There, a cancel refused while the query was flowing has to leave room for another once the stream is paused (
should report a cancel of a paused stream although an earlier cancel was refused).To share the version check, #1938's
driverHoldsPausedQueriesnow reads the major version through adriverMajorVersionhelper.Tests
Unit tests (
test/common/unit.js), with a mock that takes one cancel and refuses the next the way 6.1 does:ECANCEL, and msnodesqlv8 is sent one cancel.ECANCELand completes without being resumed.Both fail without the fix. One mutant applies the one-cancel rule to every version, and an existing 5.x test fails on it, so the version gate is needed.
Integration test:
cancelling a request twice as soon as it is senthas a promise and a paused-stream variant. Each runs on both drivers on a pool of one connection, and checks that the next query runs. msnodesqlv8 < 5.6 cancels only a paused query, so there the promise variant expects the request to run to the end, ascancel a running requestalready does.With tedious, a paused stream cancelled just as its batch starts gets SQL Server's "the batch is aborted" error before the
ECANCEL, even with a single cancel. That was already so before this change. The test therefore checks that the request ends with exactly oneECANCEL.Live results (Windows CI, SQL Server 2022)
These ran on a throwaway fork branch, since deleted. The two new integration tests ran three times without the fix (#1938's adapter) and three times with it. The full msnodesqlv8 suite then ran with the fix.
EREQUEST, and the paused stream never completedEarlier probes found the same failure on 6.0.0, 6.1.0 and 6.2.0, in three runs out of three each. They covered master's adapter, and #1938 with #1928 on top.
Local checks
npm run lint: clean.npm run test-unit: 290 passing.test-tediousagainst Azure SQL Edge: 182 passing. The 3 failures are the CLR-dependent tests that always fail on SQL Edge.Part of #1940.
🤖 Generated with Claude Code