From eb5d72b4c4f16171cd09f2786cd2df748fefce99 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sat, 12 Sep 2026 18:39:01 +0200 Subject: [PATCH 1/2] Enable-DbaAgHadr, Disable-DbaAgHadr - Report a refused service restart instead of ignoring it With -Force both commands stop and start Agent and Engine through Stop-DbaService and Start-DbaService and discard the results. A refused stop or start only shows as Status "Failed" on those objects, so when the Agent was slow to stop on a CI runner the engine restart was refused, the Agent ended up stopped, and the command still reported success. Six minutes later an unrelated test failed with "SQL Server Agent is not running". The results are now checked and any failure goes through the existing "Issue restarting" Stop-Function with the service messages as error record: a warning without -EnableException, an exception with it. The two CI instance setup scripts that run Enable-DbaAgHadr -Force now handle that failure: wait for a StopPending service to settle, start what is not running, and try once more. (do Enable-DbaAgHadr, Disable-DbaAgHadr) Co-Authored-By: Claude Fable 5.1 --- public/Disable-DbaAgHadr.ps1 | 19 ++++++++++++++++--- public/Enable-DbaAgHadr.ps1 | 19 ++++++++++++++++--- tests/appveyor.SQL2019.ps1 | 21 ++++++++++++++++++++- tests/appveyor.SQL2022.ps1 | 21 ++++++++++++++++++++- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/public/Disable-DbaAgHadr.ps1 b/public/Disable-DbaAgHadr.ps1 index 3161e8c83d03..583fecf65752 100644 --- a/public/Disable-DbaAgHadr.ps1 +++ b/public/Disable-DbaAgHadr.ps1 @@ -128,10 +128,23 @@ function Disable-DbaAgHadr { if (Test-Bound 'Force') { if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting Engine and Agent service for $instance on $computerFullName")) { try { - $null = Stop-DbaService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine - $null = Start-DbaService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine + # A refused stop or start only shows as Status "Failed" on the returned objects, so they have + # to be checked. Without that, an Agent that was slow to stop left the engine restart refused + # and the Agent stopped, while the command still reported success. + $splatRestart = @{ + ComputerName = $computerFullName + InstanceName = $instanceName + Type = "Agent", "Engine" + EnableException = $true + } + $restartResults = @(Stop-DbaService @splatRestart) + $restartResults += Start-DbaService @splatRestart + $restartFailures = @($restartResults | Where-Object Status -notlike "Successful*") + if ($restartFailures.Count -gt 0) { + throw (($restartFailures | ForEach-Object { "$($PSItem.ServiceName): $($PSItem.Message)" }) -join " | ") + } } catch { - Stop-Function -Message "Issue restarting $instance" -Target $instance -Continue + Stop-Function -Message "Issue restarting $instance" -ErrorRecord $_ -Target $instance -Continue } } } diff --git a/public/Enable-DbaAgHadr.ps1 b/public/Enable-DbaAgHadr.ps1 index d135df7d09dc..0f2971443dee 100644 --- a/public/Enable-DbaAgHadr.ps1 +++ b/public/Enable-DbaAgHadr.ps1 @@ -131,10 +131,23 @@ function Enable-DbaAgHadr { if (Test-Bound -ParameterName Force) { if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting Engine and Agent service for $instance on $computerFullName")) { try { - $null = Stop-DbaService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine - $null = Start-DbaService -ComputerName $computerFullName -InstanceName $instanceName -Type Agent, Engine + # A refused stop or start only shows as Status "Failed" on the returned objects, so they have to + # be checked. Without that, an Agent that was slow to stop left the engine restart refused and + # the Agent stopped, while the command still reported success. + $splatRestart = @{ + ComputerName = $computerFullName + InstanceName = $instanceName + Type = "Agent", "Engine" + EnableException = $true + } + $restartResults = @(Stop-DbaService @splatRestart) + $restartResults += Start-DbaService @splatRestart + $restartFailures = @($restartResults | Where-Object Status -notlike "Successful*") + if ($restartFailures.Count -gt 0) { + throw (($restartFailures | ForEach-Object { "$($PSItem.ServiceName): $($PSItem.Message)" }) -join " | ") + } } catch { - Stop-Function -Message "Issue restarting $instance" -Target $instance -Continue + Stop-Function -Message "Issue restarting $instance" -ErrorRecord $_ -Target $instance -Continue } } } diff --git a/tests/appveyor.SQL2019.ps1 b/tests/appveyor.SQL2019.ps1 index 1e367175ccf5..8d43b36266f4 100644 --- a/tests/appveyor.SQL2019.ps1 +++ b/tests/appveyor.SQL2019.ps1 @@ -18,7 +18,26 @@ Write-Host -Object "$indent Configuring $instance" -ForegroundColor DarkGreen $null = Set-DbaSpConfigure -SqlInstance $sqlinstance -Name RemoteDacConnectionsEnabled -Value $true -EnableException $null = Set-DbaSpConfigure -SqlInstance $sqlinstance -Name ExtensibleKeyManagementEnabled -Value $true -EnableException Invoke-DbaQuery -SqlInstance $sqlinstance -Query "CREATE CRYPTOGRAPHIC PROVIDER dbatoolsci_AKV FROM FILE = 'C:\github\appveyor-lab\keytests\ekm\Microsoft.AzureKeyVaultService.EKM.dll'" -EnableException -$null = Enable-DbaAgHadr -SqlInstance $sqlinstance -Force -EnableException -Confirm:$false +# Enable-DbaAgHadr -Force stops and starts Agent and Engine. When the Agent is slow to stop, the engine restart is +# refused and the Agent can be left stopped; the command reports that as an error, so settle both services, start +# them again and try once more. +try { + $null = Enable-DbaAgHadr -SqlInstance $sqlinstance -Force -EnableException -Confirm:$false +} catch { + Write-Host -Object "$indent Enabling HADR on $instance failed ($($PSItem.Exception.Message)), starting the services and retrying" -ForegroundColor DarkYellow + foreach ($serviceName in ("MSSQL$" + $instance), ("SQLAgent$" + $instance)) { + $service = Get-Service -Name $serviceName + if ($service.Status -eq "StopPending") { + $service.WaitForStatus("Stopped", "00:03:00") + } + $service.Refresh() + if ($service.Status -ne "Running") { + Start-Service -Name $serviceName + $service.WaitForStatus("Running", "00:03:00") + } + } + $null = Enable-DbaAgHadr -SqlInstance $sqlinstance -Force -EnableException -Confirm:$false +} Invoke-DbaQuery -SqlInstance $sqlinstance -Query "CREATE MASTER KEY ENCRYPTION BY PASSWORD = ''" -EnableException Invoke-DbaQuery -SqlInstance $sqlinstance -Query "CREATE CERTIFICATE dbatoolsci_AGCert WITH SUBJECT = 'AG Certificate'" -EnableException diff --git a/tests/appveyor.SQL2022.ps1 b/tests/appveyor.SQL2022.ps1 index 42e88115d58a..c606f800dc17 100644 --- a/tests/appveyor.SQL2022.ps1 +++ b/tests/appveyor.SQL2022.ps1 @@ -18,7 +18,26 @@ Write-Host -Object "$indent Configuring $instance" -ForegroundColor DarkGreen $null = Set-DbaSpConfigure -SqlInstance $sqlinstance -Name RemoteDacConnectionsEnabled -Value $true -EnableException $null = Set-DbaSpConfigure -SqlInstance $sqlinstance -Name ExtensibleKeyManagementEnabled -Value $true -EnableException Invoke-DbaQuery -SqlInstance $sqlinstance -Query "CREATE CRYPTOGRAPHIC PROVIDER dbatoolsci_AKV FROM FILE = 'C:\github\appveyor-lab\keytests\ekm\Microsoft.AzureKeyVaultService.EKM.dll'" -EnableException -$null = Enable-DbaAgHadr -SqlInstance $sqlinstance -Force -EnableException -Confirm:$false +# Enable-DbaAgHadr -Force stops and starts Agent and Engine. When the Agent is slow to stop, the engine restart is +# refused and the Agent can be left stopped; the command reports that as an error, so settle both services, start +# them again and try once more. +try { + $null = Enable-DbaAgHadr -SqlInstance $sqlinstance -Force -EnableException -Confirm:$false +} catch { + Write-Host -Object "$indent Enabling HADR on $instance failed ($($PSItem.Exception.Message)), starting the services and retrying" -ForegroundColor DarkYellow + foreach ($serviceName in ("MSSQL$" + $instance), ("SQLAgent$" + $instance)) { + $service = Get-Service -Name $serviceName + if ($service.Status -eq "StopPending") { + $service.WaitForStatus("Stopped", "00:03:00") + } + $service.Refresh() + if ($service.Status -ne "Running") { + Start-Service -Name $serviceName + $service.WaitForStatus("Running", "00:03:00") + } + } + $null = Enable-DbaAgHadr -SqlInstance $sqlinstance -Force -EnableException -Confirm:$false +} Invoke-DbaQuery -SqlInstance $sqlinstance -Query "CREATE MASTER KEY ENCRYPTION BY PASSWORD = ''" -EnableException Invoke-DbaQuery -SqlInstance $sqlinstance -Query "CREATE CERTIFICATE dbatoolsci_AGCert WITH SUBJECT = 'AG Certificate'" -EnableException From f9626ffdf6294fc5f6af5f8e297acdefab9c5a8c Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Mon, 14 Sep 2026 20:55:23 +0200 Subject: [PATCH 2/2] Enable-DbaAgHadr, Disable-DbaAgHadr - Start the Agent after the forced restart only if it was running before The forced restart stopped and started Agent and Engine unconditionally, so an Agent that had been stopped on purpose came back running, an old defect noticed in review. Read the Agent state before the restart: the Agent still stops with the engine it depends on, but it is started again only if it had been running. Both test files gain a test that stops the Agent, runs the command with -Force and checks that the Agent is still stopped, and their AfterAll starts the Agent again in any case. (do Enable-DbaAgHadr, Disable-DbaAgHadr) Co-Authored-By: Claude Fable 5.1 --- public/Disable-DbaAgHadr.ps1 | 22 ++++++++++++++-------- public/Enable-DbaAgHadr.ps1 | 22 ++++++++++++++-------- tests/Disable-DbaAgHadr.Tests.ps1 | 15 +++++++++++++++ tests/Enable-DbaAgHadr.Tests.ps1 | 16 ++++++++++++++++ 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/public/Disable-DbaAgHadr.ps1 b/public/Disable-DbaAgHadr.ps1 index 583fecf65752..1ee1777e1399 100644 --- a/public/Disable-DbaAgHadr.ps1 +++ b/public/Disable-DbaAgHadr.ps1 @@ -126,19 +126,25 @@ function Disable-DbaAgHadr { } } if (Test-Bound 'Force') { - if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting Engine and Agent service for $instance on $computerFullName")) { + if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting the Engine service, and the Agent service if it is running, for $instance on $computerFullName")) { try { - # A refused stop or start only shows as Status "Failed" on the returned objects, so they have - # to be checked. Without that, an Agent that was slow to stop left the engine restart refused - # and the Agent stopped, while the command still reported success. - $splatRestart = @{ + # The Agent depends on the engine, so it is stopped with it, but it is only started again if + # it was running before: a stopped Agent used to come back running from this restart. + $splatServices = @{ ComputerName = $computerFullName InstanceName = $instanceName - Type = "Agent", "Engine" EnableException = $true } - $restartResults = @(Stop-DbaService @splatRestart) - $restartResults += Start-DbaService @splatRestart + $agentWasRunning = (Get-DbaService @splatServices -Type Agent).State -eq "Running" + # A refused stop or start only shows as Status "Failed" on the returned objects, so they have + # to be checked. Without that, an Agent that was slow to stop left the engine restart refused + # and the Agent stopped, while the command still reported success. + $restartResults = @(Stop-DbaService @splatServices -Type "Agent", "Engine") + if ($agentWasRunning) { + $restartResults += Start-DbaService @splatServices -Type "Agent", "Engine" + } else { + $restartResults += Start-DbaService @splatServices -Type "Engine" + } $restartFailures = @($restartResults | Where-Object Status -notlike "Successful*") if ($restartFailures.Count -gt 0) { throw (($restartFailures | ForEach-Object { "$($PSItem.ServiceName): $($PSItem.Message)" }) -join " | ") diff --git a/public/Enable-DbaAgHadr.ps1 b/public/Enable-DbaAgHadr.ps1 index 0f2971443dee..f58861847bc7 100644 --- a/public/Enable-DbaAgHadr.ps1 +++ b/public/Enable-DbaAgHadr.ps1 @@ -129,19 +129,25 @@ function Enable-DbaAgHadr { } if (Test-Bound -ParameterName Force) { - if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting Engine and Agent service for $instance on $computerFullName")) { + if ($PSCmdlet.ShouldProcess($instance, "Force provided, restarting the Engine service, and the Agent service if it is running, for $instance on $computerFullName")) { try { - # A refused stop or start only shows as Status "Failed" on the returned objects, so they have to - # be checked. Without that, an Agent that was slow to stop left the engine restart refused and - # the Agent stopped, while the command still reported success. - $splatRestart = @{ + # The Agent depends on the engine, so it is stopped with it, but it is only started again if it + # was running before: a stopped Agent used to come back running from this restart. + $splatServices = @{ ComputerName = $computerFullName InstanceName = $instanceName - Type = "Agent", "Engine" EnableException = $true } - $restartResults = @(Stop-DbaService @splatRestart) - $restartResults += Start-DbaService @splatRestart + $agentWasRunning = (Get-DbaService @splatServices -Type Agent).State -eq "Running" + # A refused stop or start only shows as Status "Failed" on the returned objects, so they have to + # be checked. Without that, an Agent that was slow to stop left the engine restart refused and + # the Agent stopped, while the command still reported success. + $restartResults = @(Stop-DbaService @splatServices -Type "Agent", "Engine") + if ($agentWasRunning) { + $restartResults += Start-DbaService @splatServices -Type "Agent", "Engine" + } else { + $restartResults += Start-DbaService @splatServices -Type "Engine" + } $restartFailures = @($restartResults | Where-Object Status -notlike "Successful*") if ($restartFailures.Count -gt 0) { throw (($restartFailures | ForEach-Object { "$($PSItem.ServiceName): $($PSItem.Message)" }) -join " | ") diff --git a/tests/Disable-DbaAgHadr.Tests.ps1 b/tests/Disable-DbaAgHadr.Tests.ps1 index e1a94d5301d6..b628b003f5e7 100644 --- a/tests/Disable-DbaAgHadr.Tests.ps1 +++ b/tests/Disable-DbaAgHadr.Tests.ps1 @@ -36,6 +36,12 @@ Describe $CommandName -Tag IntegrationTests { # Re-enable HADR for future tests $null = Enable-DbaAgHadr -SqlInstance $TestConfig.InstanceHadr -Force + # The Agent test stops the Agent, so make sure it is running again whatever happened in between. Collected first, + # because Start-DbaService with nothing piped in falls back to the local machine. + $stoppedAgent = Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent | Where-Object State -ne "Running" + if ($stoppedAgent) { + $null = $stoppedAgent | Start-DbaService + } $PSDefaultParameterValues.Remove("*-Dba*:EnableException") } @@ -45,5 +51,14 @@ Describe $CommandName -Tag IntegrationTests { $disableResults = Disable-DbaAgHadr -SqlInstance $TestConfig.InstanceHadr -Force $disableResults.IsHadrEnabled | Should -BeFalse } + + It "Leaves a stopped Agent stopped when -Force restarts the engine" { + # The forced restart used to start the Agent with the engine whether it had been running before or not. + $null = Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent | Stop-DbaService + $disableResults = Disable-DbaAgHadr -SqlInstance $TestConfig.InstanceHadr -Force + $disableResults.IsHadrEnabled | Should -BeFalse + (Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent).State | Should -Be "Stopped" + $null = Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent | Start-DbaService + } } } \ No newline at end of file diff --git a/tests/Enable-DbaAgHadr.Tests.ps1 b/tests/Enable-DbaAgHadr.Tests.ps1 index d998b1b0c471..cd91861780ef 100644 --- a/tests/Enable-DbaAgHadr.Tests.ps1 +++ b/tests/Enable-DbaAgHadr.Tests.ps1 @@ -37,6 +37,13 @@ Describe $CommandName -Tag IntegrationTests { # We want to run all commands in the AfterAll block with EnableException to ensure that the test fails if the cleanup fails. $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + # The Agent test stops the Agent, so make sure it is running again whatever happened in between. Collected first, + # because Start-DbaService with nothing piped in falls back to the local machine. + $stoppedAgent = Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent | Where-Object State -ne "Running" + if ($stoppedAgent) { + $null = $stoppedAgent | Start-DbaService + } + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") } @@ -45,5 +52,14 @@ Describe $CommandName -Tag IntegrationTests { $results = Enable-DbaAgHadr -SqlInstance $TestConfig.InstanceHadr -Force $results.IsHadrEnabled | Should -BeTrue } + + It "Leaves a stopped Agent stopped when -Force restarts the engine" { + # The forced restart used to start the Agent with the engine whether it had been running before or not. + $null = Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent | Stop-DbaService + $results = Enable-DbaAgHadr -SqlInstance $TestConfig.InstanceHadr -Force + $results.IsHadrEnabled | Should -BeTrue + (Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent).State | Should -Be "Stopped" + $null = Get-DbaService -SqlInstance $TestConfig.InstanceHadr -Type Agent | Start-DbaService + } } } \ No newline at end of file