Skip to content

fix: stop msnodesqlv8 streams losing rows under back pressure - #1919

Merged
dhensby merged 2 commits into
tediousjs:masterfrom
dhensby:fix/msnodesqlv8-stream-back-pressure
Oct 14, 2026
Merged

dhensby merged 2 commits into
tediousjs:masterfrom
dhensby:fix/msnodesqlv8-stream-back-pressure

Conversation

@dhensby

@dhensby dhensby commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

What this does:

Piping a msnodesqlv8 stream (request.pipe() / toReadableStream()) under back pressure silently loses rows. msnodesqlv8 fetches a result set's rows 50 at a time. Paused part way through a result set's last, short batch, it moves on without the rest of that batch. Back pressure pauses a stream on almost every row, so any result set whose row count isn't a multiple of 50 is affected. The existing query with pipe and back pressure test uses top 2000, so it never hit this.

Confirmed on Windows CI (SQL Server 2022, a writer taking one row every 10 ms) in run 38378595926:

2.7.0 / 3.1.0 / 4.5.0 5.2.1 5.6.0
top 2010 last 9 rows lost last 9 rows lost ok
top 75; top 75 rows 52–75 emitted under the second set's column, last 24 rows lost rows 52–75 under the second set's column, second set skipped ok
top 51; top 51 ok second set skipped second set skipped

None of these raise an error, and the connection is usable afterwards. tedious delivers every row in every case.

The fix is in the msnodesqlv8 adapter. v2–v4 and 5.2.1 (the last v5 release with a Node 20 prebuild) won't get upstream fixes, and 5.6.0 is still affected.

  • The driver is paused only once it has handed over a whole batch (every 50th row of a result set), or before it has read anything. Those are the only points where every version is safe to pause. This couples the adapter to the driver's batch size; DRIVER_ROW_BATCH_SIZE explains why.
  • While a stream is paused, everything it would emit waits in a queue: rows, recordsets, info, errors, rows affected, and the completion. On resume these are emitted in order. The connection is still released as soon as the driver finishes.

Cancel had to change with it. The driver now keeps reading for a while after a stream is paused, so it can be in the middle of a fetch, moving to the next result set, or ending the query when the stream is cancelled. msnodesqlv8 doesn't cope with being paused or cancelled at those points. On 5.6 it frees the statement twice and drops an extra queue operation, which can be the next request's query. On 5.0–5.5 the stream gets a spurious HY010 error. So:

  • A paused stream whose driver is still reading is cancelled when the driver next stops, at the end of a batch or the start of a result set.
  • A query that ends first, or times out, completes as cancelled. This includes a query ending on an error, which msnodesqlv8 signals with free and no done.
  • In every other case (not streaming, a stream that isn't paused, or a driver that is already parked), cancel() behaves as it does on master.
  • A cancelled paused stream drops the rows still waiting and reports ECANCEL, also when the query had already ended.

Behaviour change to note:

  • Nothing is emitted while a stream is paused. Before, a row that arrived while it was paused was dropped, and other events leaked through. The README now says so.
  • After pause(), msnodesqlv8 keeps reading to the end of a full batch, and those rows are held until resume().
  • A paused stream's cancel waits for the driver to stop reading. Its rows are already in memory, so this costs nothing. But a long statement between result sets is not interrupted any sooner than the request timeout. The README documents this.

Known limit (driver-side, same on master): on 5.6, a stream that is paused before its query starts and then cancelled while the statement is still executing leaves msnodesqlv8's reader paused. Its work queue stays blocked unless something resumes it. Master behaves the same unless the caller explicitly calls resume() after cancelling.

Testing:

  • Lint and the unit tests (181) pass locally. The new stub-driven tests in msnodesqlv8 query events › stream back pressure play a query the way each reader generation does (2.x–4.x, 5.0–5.5, 5.6). That includes refused and dropped cancels, the cancel token msnodesqlv8 holds before a statement exists, stuck fetches, timeouts, and queries that end on an error. Each fails against the adapter before its fix.
  • A new integration test, query with pipe and back pressure across partial row batches, is wired for both drivers. It pipes result sets of 75, 51 and 120 rows into a writer that takes one row every 10 ms. The writer has to be timer-paced: a setImmediate one drains faster than the driver fetches, and misses the 5.6.0 case.
  • The tedious integration suite and the CLI tests pass locally against Azure SQL Edge. The exceptions are user defined types and the two bulk-load tests for spatial and hierarchyid columns from fix: bulk load created tables and Xml columns on tedious #1914. Those three need CLR, which SQL Edge doesn't have, and fail the same way on master.
  • The back-pressure fix was checked against real msnodesqlv8 on Windows. A throwaway fork branch ran the fix and master's adapter in the same job for each version:
    • Run 38380411913: with the fix, the full msnodesqlv8 suite passes on ^2, ^3, ^4 and 5.2.1 (135 tests). Master's adapter fails only the new test.
    • Run 38382240427: the new test passes 3 out of 3 times with the fix. With master's adapter it fails on every version, 5.6.0 included.
    • Those runs predate the cancel changes, so this PR's own Windows CI is the first run of the final version against the real driver.
  • The cancel changes went through six review rounds with adversarial verification. Every finding was reproduced, or refuted, by running the real published msnodesqlv8 lib/*.js (2.7.0, 4.5.0, 5.2.1, 5.5.1, 5.6.0) over a fake native layer, and comparing against master's adapter. The last round found nothing new.

Related issues:

Pre/Post merge checklist:

  • Update change log

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cancellation can emit queued rows after a listener resumes reentrantly, and the integration query has nondeterministic ordering.

3 open findings
What changed in this PR

Fixes row loss in msnodesqlv8 streams under back pressure and adjusts paused-stream cancellation behavior.

Changes:

  • Queues stream events while paused and pauses the driver at safe batch boundaries.
  • Defers cancellation until the driver can safely stop.
  • Adds unit, integration, and API documentation coverage.
File Description
lib/​msnodesqlv8/​request.js Implements buffered events and safe pause/cancel handling.
README.md Documents paused-stream and cancellation behavior.
test/​common/​unit.js Adds driver-version event simulations.
test/​common/​tests.js Adds partial-batch back-pressure coverage.
test/​msnodesqlv8/​msnodesqlv8.js Registers the integration test.
test/​tedious/​tedious.js Registers cross-driver regression coverage.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/msnodesqlv8/request.js
Comment thread test/common/tests.js Outdated
Comment thread README.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The version-specific pause and cancellation state machine requires final Windows CI validation against real msnodesqlv8 releases.

0 open findings

3 resolved since last review

🧠 Review effort: Balanced

@dhensby
dhensby force-pushed the fix/msnodesqlv8-stream-back-pressure branch from 618555e to 471163f Compare October 13, 2026 19:28
dhensby and others added 2 commits October 13, 2026 21:22
msnodesqlv8 fetches a result set's rows 50 at a time. Paused part way through
a result set's last, short batch, it moves on without the rest of that batch.
On the last result set it ends the query, and the rows still to come are never
emitted. On an earlier one it starts the next result set, and on resume emits
the rest of the batch under the next result set's columns; from 5.0 it then
skips the next result set as well. 5.6 waits for the rest of a batch, but when
paused on the only row of a result set's last batch it still skips the next
result set.

Back pressure from pipe() and toReadableStream() pauses a stream on almost
every row, so a piped result set whose row count is not a multiple of 50
silently lost up to 49 rows.

The driver is now paused only once it has handed over a whole batch, or before
it has read anything. Rows that arrive in the meantime are held, along with
anything else the stream emits after them, and emitted in order once the
stream is resumed, and the stream completes once it has emitted them. Nothing
is emitted while a stream is paused; before, a row that arrived while it was
paused was dropped.

msnodesqlv8 cancels a query cleanly only once it has stopped reading: paused
or cancelled part way through a fetch, or as it moves to the next result set
or ends the query, it can free the statement twice and break the queue of the
connection. So a paused stream whose driver is still reading is cancelled when
the driver next stops, at the end of a batch or the start of a result set, and
a query that ends first, or times out, completes as cancelled. A paused stream
that is cancelled drops the rows still waiting and reports ECANCEL, also when
the query had already ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dhensby
dhensby force-pushed the fix/msnodesqlv8-stream-back-pressure branch from bc33026 to 5e7322f Compare October 13, 2026 20:22
@dhensby
dhensby merged commit 2442aa9 into tediousjs:master Oct 14, 2026
47 checks passed
@dhensby
dhensby deleted the fix/msnodesqlv8-stream-back-pressure branch October 14, 2026 07:44
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 12.7.7 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants