Skip to content

Read-DbaBackupHeader, Start-/Stop-DbaDbEncryption - Run the threads in a pool without a host and close it in a finally - #10789

Merged
potatoqualitee merged 3 commits into
developmentfrom
runspace-pool-without-host
Oct 19, 2026
Merged

potatoqualitee merged 3 commits into
developmentfrom
runspace-pool-without-host

Conversation

@andreasjordan

@andreasjordan andreasjordan commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #10776: section 1 and the pool items of section 2.

The problem

Read-DbaBackupHeader (and with it Restore-DbaDatabase and Get-DbaBackupInformation) and Start-/Stop-DbaDbEncryption -Parallel created their runspace pool with CreateRunspacePool(1, 10, $initialSessionState, $Host). Once a thread of that pool had run, the warnings, verbose messages and progress records of the calling pipeline no longer reached the streams of a host created through the PowerShell API (automation hosts, SDK hosts, notebooks). In a console nobody notices, because the records go to the console UI. Through the API, a Restore-DbaDatabase loses every warning written after the backup headers were read, including the warnings of Stop-Function.

The pool was also closed only after the loop that receives the threads, and a failed thread skipped its Dispose(). A throw under -EnableException, Ctrl+C or Select-Object -First left the pool and its runspaces open, and Read-DbaBackupHeader left [Runspace]::DefaultRunspace unrestored.

The fix

  • The pool is created without a host: CreateRunspacePool($initialSessionState) plus SetMaxRunspaces(10). There is no overload with minimum, maximum and session state but no host, and the minimum is 1 by default.
  • Nothing gets lost on the console side. The threads of Read-DbaBackupHeader write output and errors only, and both are read from the thread itself. The encryption threads catch every failure, including the throw of Connect-DbaInstance, and return it as an object that the caller reports with Stop-Function.
  • The try now starts before the threads are created. Its finally disposes every thread (a thread that still runs is stopped by Dispose()), then closes and disposes the pool. Read-DbaBackupHeader also restores the default runspace there.

Most of the diff is indentation: the loop that creates the threads moved into the try. git diff -w shows the real change. The single quotes in the re-indented lines of Read-DbaBackupHeader became double quotes.

Read-DbaBackupHeader: an incomplete header returned the file anyway

Found while re-checking the command, not in #10776. The Stop-Function -Continue for a row with BackupName *** INCOMPLETE *** ("appears to be from a new version of SQL Server ..., skipping") sat in the loop over the rows, so it only skipped the rest of that loop and the file was returned anyway. The rows are checked first now, and the -Continue skips the file, as its message says.

There is no regression test for this one: older SQL Server versions write that marker for a backup of a newer one. SQL Server 2019 reading a 2025 backup returns an empty BackupName and DatabaseVersion 998 instead (checked in the lab), and neither the lab nor CI has an instance older than 2019.

Tests

Every new context runs the command in a host created through the PowerShell API, the way the progress tests of #10773-#10775 do:

  • Read-DbaBackupHeader: a file the instance can open but that holds no backup. Its thread fails, and the "Problem found with" warning written afterwards must reach the host's warning stream, as must the completion of progress bar Id 1. A second context stops the command with Select-Object -First 1 and checks that no runspace is left open.
  • Start-DbaDbEncryption and Stop-DbaDbEncryption: -Parallel with Select-Object -First 1 checks the result, the completion of progress bar Id 1 in the host's streams and the number of open runspaces.

The comments in the existing progress contexts that said the -Parallel bar cannot be checked this way got a line pointing to the new context.

Verified

In the lab on SQL Server 2019 (single instance) and 2022/2025 (the others), on Windows PowerShell 5.1 and PowerShell 7.6. CI runs Pester on 5.1 only.

Test file 5.1 7.6
Read-DbaBackupHeader 6 passed 6 passed
Start-DbaDbEncryption 10 passed 10 passed
Stop-DbaDbEncryption 9 passed 9 passed
Restore-DbaDatabase (caller) 86 passed, 3 skipped 86 passed, 3 skipped
Get-DbaBackupInformation (caller) 23 passed 23 passed
dbatools.Tests.ps1 81 passed, 6 skipped -

Read-DbaBackupHeader and dbatools.Tests.ps1 ran again at the last commit, with the same result.

No warnings in any of them. Against the old commands (5.1) the new tests fail with exactly the new assertions: the warning and the completed record never reach the host's streams, and Select-Object -First 1 leaves one runspace open in Read-DbaBackupHeader and two in Start-/Stop-DbaDbEncryption.

Not in this PR

  • private/functions/flowcontrol/Invoke-Parallel.ps1:450 creates its pool on $Host too (callers: Start-/Stop-/Restart-DbaService through Update-ServiceStatus, Install-/Update-DbaInstance with two or more computers, Invoke-DbaPfRelog). There the threads do write warnings and progress that a console user sees live today, and Invoke-Parallel forwards only their errors, so it needs its own decision.

🤖 Generated with Claude Code

andreasjordan and others added 2 commits October 17, 2026 14:02
…n a pool without a host and close it in a finally

The three commands created their runspace pool on $Host. Once a thread had run there, the warnings,
verbose messages and progress records of the calling pipeline no longer reached the streams of a host
created through the PowerShell API. The pool is now created without a host. The threads write no
warnings of their own, except the encryption threads when their connection fails; they now connect
with EnableException, so the reason lands in the Error of the result instead of a thread stream.

The pool was closed only after the loop that receives the threads, and a failed thread skipped its
Dispose(). A throw under EnableException, Ctrl+C or Select-Object -First left the pool open, and
Read-DbaBackupHeader did not restore the default runspace. The finally now disposes every thread
and closes and disposes the pool.

Part of #10776.

(do Read-DbaBackupHeader, Start-DbaDbEncryption, Stop-DbaDbEncryption, Restore-DbaDatabase, Get-DbaBackupInformation)

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

Connect-DbaInstance throws by default and has -DisableException instead of -EnableException, so the
threads already put the reason of a failed connection into the Error of their result. Passing
EnableException failed every thread with a parameter binding error.

(do Start-DbaDbEncryption, Stop-DbaDbEncryption)

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

The Stop-Function -Continue for a row with BackupName "*** INCOMPLETE ***" sat in the loop over the
rows, so it only skipped the rest of that loop and the file was returned anyway. The rows are checked
first now, and the -Continue skips the file, as its message says.

No regression test: older SQL Server versions write that marker for a backup of a newer one. SQL
Server 2019 reading a 2025 backup returns an empty BackupName and DatabaseVersion 998 instead, and
neither the lab nor CI has an instance older than 2019.

(do Read-DbaBackupHeader)

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

@potatoqualitee potatoqualitee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at f790dac231584131c7b07056ceb1e4ec2312fa7a. No material defect found.

Reviewed the complete six-file patch, worker/caller code, tests, description, discussions, and exact-head CI. The hostless pool preserves concurrency and parent-side warning/error reporting. The finally blocks cover failed result handling and early pipeline termination, and the added integration tests exercise API-host output and leaked-runspace detection.

Focused validation with the real PowerShell 7.6.7 runspace API confirmed worker cancellation, safe repeated disposal, and pool closure. All six changed files parsed successfully. All 20 executed head checks passed and one generic Windows check was skipped. The SQL Server integration suite was not rerun locally.

@potatoqualitee
potatoqualitee merged commit 91c6025 into development Oct 19, 2026
22 checks passed
@potatoqualitee
potatoqualitee deleted the runspace-pool-without-host branch October 19, 2026 15:33
@potatoqualitee

Copy link
Copy Markdown
Member

merci

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.

2 participants