Skip to content

fix: resume a held msnodesqlv8 query that carries on after its last error - #1939

Merged
dhensby merged 1 commit into
tediousjs:masterfrom
dhensby:fix/msnodesqlv8-detached-resume-hang
Oct 18, 2026
Merged

dhensby merged 1 commit into
tediousjs:masterfrom
dhensby:fix/msnodesqlv8-detached-resume-hang

Conversation

@dhensby

@dhensby dhensby commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

What

A stream paused before its first result set holds msnodesqlv8's query back (_pauseStream). When msnodesqlv8 gives an error with more=false, the request marks the query detached, and while it is detached the request makes no pause, resume or cancel. The 'meta' handler takes the query up again if it goes on to a result set after all: it clears detached and replays a held cancel. It never resumes the driver, though. So the query stayed parked at 'meta' if the stream was resumed while it was detached. The request then never completed, and its connection was never released.

The fix: when 'meta' clears detached, a stream that is no longer paused, with a driver still paused and nothing being cancelled, resumes the query. The resume runs a tick later, through setImmediate, and _resumeStream checks every condition again when it runs. It cannot be synchronous: msnodesqlv8's resume() fetches at once, and the reader calls dispatch() again as soon as 'meta' is handled. A resume made inside 'meta' would therefore fetch twice, which is the failure in #1924.

Reachability

This path is not reachable on any msnodesqlv8 version tested. It comes from code review and is not a reported hang.

I probed it on Windows CI against SQL Server 2022 with msnodesqlv8 2.7.0, 3.1.0, 4.5.0, 5.2.1 and 5.6.0. The probe used a throwaway fork branch, which has since been deleted.

  • Raw driver events: I ran 17 candidate batches, each as written and with a waitfor between the failing statement and the select. The batches used raiserror, print, a PK violation, divide by zero, row counts before and after the error, exec(), sp_executesql, procedures with NOCOUNT on and off, and a select ahead of the error.
    • Before 5.6: every error ends the query with error(false) and then 'free'.
    • 5.6: the batch carries on, but the error comes with more=true.
    • Never seen: an error with more=false followed by 'meta'.
  • Through node-mssql: each batch ran three times per adapter, with the stream resumed from a hook on the driver's error event, right after node-mssql handled it. That makes 255 runs on master's adapter and 255 on this branch's. Every run completed, released its connection, and left the connection able to run the next query.
    • I also tried a stream paused on the 50th row of a 50- and a 100-row first result set. A driver paused at the end of a whole batch never let the next statement's error through.
  • Full suite: npm run test-msnodesqlv8 passed on all five versions with this change.

The request already provides for this case at 'meta'. The reader of every msnodesqlv8 version before 5.6 can give a statement's last error more=false and carry on: begin() routes a warning from execute with more = i < errorCount - 1. This change finishes the job that handling started.

Related

Tests

A new case in msnodesqlv8 query events plays this sequence: fail with more=false, the stream resumes, 'meta', then a row, 'done' and 'free'. It asserts:

  • the query is still held as 'meta' is handled, which catches a synchronous resume;
  • the driver gets ['pause', 'resume'];
  • the row is streamed;
  • the connection is released once.

The test fails on master.

🤖 Generated with Claude Code

…rror

A stream paused before its first result set holds msnodesqlv8's query
back. The request takes an error with more=false to end the batch, and
takes the query up again if it goes on to a result set after all. A
stream resumed in between left the query held at that result set, so
the request never completed and its connection was never released.

The query is now resumed once it reaches the result set. That happens a
tick after 'meta', because msnodesqlv8 fetches as soon as it is resumed,
and again once 'meta' is handled.

No msnodesqlv8 version tested reaches this: 2.7, 3.1, 4.5, 5.2.1 and 5.6
against SQL Server 2022. Before 5.6 an error ahead of the first result
set ends the query, and 5.6 gives one that does not with more=true. It
is the case the request already provides for at 'meta', since the reader
of every version before 5.6 can give a statement's last error more=false
and carry on.

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

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.

🟢 Approval recommended

The focused change preserves existing lifecycle guards and has regression coverage for the intended recovery sequence.

No open issues 🎉

What changed in this PR

Completes msnodesqlv8’s recovery handling for a held query that reaches a result set after an error marked as final.

Changes:

  • Defers the driver resume until after meta handling to avoid duplicate fetches.
  • Adds regression coverage for resume timing, streamed rows, and connection release.
File Description
test/​common/​unit.js Tests the error-to-result-set recovery sequence.
lib/​msnodesqlv8/​request.js Schedules a guarded resume when a detached query continues.

Review effort: Balanced

@dhensby
dhensby merged commit f20c197 into tediousjs:master Oct 18, 2026
47 checks passed
@dhensby
dhensby deleted the fix/msnodesqlv8-detached-resume-hang branch October 18, 2026 13:39
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 12.7.10 🎉

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