Skip to content

Conversation

@ReeceGoding
Copy link
Contributor

@ReeceGoding ReeceGoding commented Nov 28, 2025

Changes

Added checks for missing read-only routing URL or list. Closes #3749.

Did not intend to add the read-only routing list check. However, the URL I gave mentions it as a requirement to make read-only routing work so there was no reason to not add it. Besides, it is pretty simple.

I was not too sure where to put these checks in the code, but they are near the most relevant check I could find.

My only concern is that this could return a lot of rows if somebody has something extreme like an 8-node cluster hosting 6 different AGs.

Demo

The change is much shorter than the demo. I promise!

This tests with a clusterless AG on Linux, as requested.

I have not done any Azure tests and probably cannot.

Destroy

Only if you mess up, clear your screen and destroy the containers.

# Think before you run this.
# clear; docker stop 2025box1; docker rm 2025box1; docker stop 2025box2; docker rm 2025box2; docker network rm AgMagic

Setup

To start, we create an environment in the broken way we want. This is PowerShell code using dbatools and Docker. If you already have a disposable AG environment, then all that you need to do is delete the read-only routing lists and URLs for that Availability Group and set a replica to be READ_ONLY when secondary. The names that SSMS uses for this don't match what the DMVs use, so don't worry if you're using the GUI for this and have to guess a bit.

Note the one part you need to change.

### vvv YOU NEED TO CHANGE THIS TO POINT TO WHERE YOU HAVE StackOverflow2010.mdf and StackOverflow2010_log.ldf
$StackOverflowDataPath = '/MyStuff/SQL/StackOverflow2010/StackOverflow2010.mdf' ### <--YOU NEED TO CHANGE THIS
$StackOverflowLogPath = '/MyStuff/SQL/StackOverflow2010/StackOverflow2010_log.ldf' ### <--YOU NEED TO CHANGE THIS
### ^^^ YOU NEED TO CHANGE THIS

docker pull mcr.microsoft.com/mssql/server:2025-latest

# I could not get the read-only routing to work without adding IPs.
docker network create --subnet 172.20.0.0/24 AgMagic

docker run -e "ACCEPT_EULA=Y" `
-e "MSSQL_ENABLE_HADR=1" `
-e "MSSQL_AGENT_ENABLED=true" `
-e "MSSQL_SA_PASSWORD=ILuvDbat00ls" `
-p 1501:1433 `
--volume shared:/shared:z `
--name 2025box1 --hostname 2025box1 `
--network AgMagic `
--ip 172.20.0.10 `
-d mcr.microsoft.com/mssql/server:2025-latest

docker run -e "ACCEPT_EULA=Y" `
-e "MSSQL_ENABLE_HADR=1" `
-e "MSSQL_AGENT_ENABLED=true" `
-e "MSSQL_SA_PASSWORD=ILuvDbat00ls" `
-p 1502:1433 `
--volume shared:/shared:z `
--name 2025box2 --hostname 2025box2 `
--network AgMagic `
--ip 172.20.0.20 `
-d mcr.microsoft.com/mssql/server:2025-latest

docker start 2025box1
docker start 2025box2

Start-Sleep 10

docker cp $StackOverflowDataPath 2025box1:/var/opt/mssql/data/StackOverflow2010.mdf
docker cp $StackOverflowLogPath 2025box1:/var/opt/mssql/data/StackOverflow2010_log.ldf

$password = ConvertTo-SecureString "ILuvDbat00ls" -AsPlainText
$cred = [PsCredential]::New("sa", $password)
$primary = Connect-DbaInstance -SqlInstance localhost:1501 -SqlCredential $cred -TrustServerCertificate
$secondary = Connect-DbaInstance -SqlInstance localhost:1502 -SqlCredential $cred -TrustServerCertificate

$fileStructure = New-Object System.Collections.Specialized.StringCollection
$fileStructure.Add("/var/opt/mssql/data/StackOverflow2010.mdf")
$filestructure.Add("/var/opt/mssql/data/StackOverflow2010_log.ldf")
Mount-DbaDatabase -SqlInstance $primary -Database StackOverflow2010 -FileStructure $fileStructure

Set-DbaDbRecoveryModel -SqlInstance $primary -Database StackOverflow2010 -RecoveryModel Full -Confirm:$false

New-DbaDbMasterKey -SqlInstance $primary, $secondary -Credential $cred -Confirm:$false

New-DbaDbCertificate -SqlInstance $primary -Name mirror -Subject mirror -Confirm:$false
$cert = (Backup-DbaDbCertificate -SqlInstance $primary -Suffix $null -Certificate mirror -Path '/shared' -EncryptionPassword $password -Confirm:$false).Path
Restore-DbaDbCertificate -SqlInstance $secondary -Path $cert -DecryptionPassword $password -Confirm:$false

New-DbaEndpoint -SqlInstance $primary, $secondary -Name mirror -Certificate mirror -Port 5022
Start-DbaEndpoint -SqlInstance $primary, $secondary -EndPoint mirror

Backup-DbaDatabase -SqlInstance $primary -Database StackOverflow2010

$params = @{
    Primary = $primary
    Secondary = $secondary
    Name = "test-ag"
    Database = "StackOverflow2010"
    ClusterType = "None"
    SeedingMode = "Automatic"
    FailoverMode = "Manual"
    IPAddress = "172.20.0.10" ### IMPORTANT
    Port = 1434 ### IMPORTANT
    ConnectionModeInSecondaryRole = "AllowReadIntentConnectionsOnly" ### IMPORTANT
    Confirm = $false
 }
New-DbaAvailabilityGroup @params

Get-DbaAgDatabase -SqlInstance $primary
Get-DbaAgDatabase -SqlInstance $secondary

If the last two lines weren't a total failure, then we are ready.

image

Verify Broken State

Now that we have a broken environment, confirm it is broken.

Step one, see what the DMVs say.

Invoke-DbaQuery -SqlInstance $primary, $secondary -Query 'SELECT replica_server_name, secondary_role_allow_connections_desc, read_only_routing_url FROM sys.availability_replicas' -AppendServerInstance
image

Good. The column that we want to be blank is blank.

Step two, run a query that should hit the replica, but does not.

$listener = Connect-DbaInstance -SqlInstance '172.20.0.10:1434' -SqlCredential $cred -TrustServerCertificate -ApplicationIntent ReadOnly
Invoke-DbaQuery -SqlInstance $listener -Query 'SELECT TOP (2) @@SERVERNAME, DisplayName FROM dbo.Users' -Database StackOverflow2010 -ReadOnly -AppendServerInstance
image

Good. It hit box1, which is the primary.

Step three, make sure my first new sp_Blitz check works. You need to install my sp_blitz, so change the file path.

### vvv YOU NEED TO CHANGE THIS
Invoke-DbaQuery -SqlInstance $primary, $secondary -File '/MyStuff/SQL/SQL-Server-First-Responder-Kit/sp_Blitz.sql'
### ^^^ YOU NEED TO CHANGE THIS
Invoke-DbaQuery -SqlInstance $primary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
Invoke-DbaQuery -SqlInstance $secondary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
image

This only fires one of the two new check. This is what we expect. Isn't it nice that PowerShell makes this easy to filter and screenshot?

We can also see that the primary and secondary give the same results. That is good.

Fix it

Step four, fix the listener halfway, so we fire only the second new check.

Set-DbaAgReplica -SqlInstance $primary -AvailabilityGroup test-ag -Replica 2025box1 -ReadonlyRoutingConnectionUrl "TCP://172.20.0.1:1501"
Set-DbaAgReplica -SqlInstance $primary -AvailabilityGroup test-ag -Replica 2025box2 -ReadonlyRoutingConnectionUrl "TCP://172.20.0.1:1502"

Step five, check sp_Blitz again.

Invoke-DbaQuery -SqlInstance $primary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
Invoke-DbaQuery -SqlInstance $secondary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
image

This is what we expect.

Step six, fix the listener just a little more.

# Dbatools has a bug, so I cannot just set 2025box2 in the routing list. See dbatools issue #9987
# Even what is returned by this is wrong. It reports a blank ReadOnlyRoutingList.
Set-DbaAgReplica -SqlInstance $primary -AvailabilityGroup test-ag -Replica 2025box1 -ReadonlyRoutingList @('2025box2', '2025box1')

If the above is right, then my new check should fire only for box2.

Step seven, check sp_Blitz again.

Invoke-DbaQuery -SqlInstance $primary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
Invoke-DbaQuery -SqlInstance $secondary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
image

This is as we expected.

Step eight, fix the listener for the last time.

Set-DbaAgReplica -SqlInstance $primary -AvailabilityGroup test-ag -Replica 2025box2 -ReadonlyRoutingList @('2025box1', '2025box2')

Prove it is fixed

Step nine, hit the secondary through the listener.

Invoke-DbaQuery -SqlInstance $listener -Query 'SELECT TOP (2) @@SERVERNAME, DisplayName FROM dbo.Users' -Database StackOverflow2010 -ReadOnly -AppendServerInstance
image

This hits box2, as expected. Recall that we hit box1 when we did this earlier.

Step ten, make sure my new sp_Blitz checks do not fire.

Invoke-DbaQuery -SqlInstance $primary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
Invoke-DbaQuery -SqlInstance $secondary -Query 'EXEC sp_Blitz' | Where CheckID -in (273,274)
image

And we're done!

As a bonus, it is good to know that although SQL Server 2025 lets us revert to the default READ_ONLY_ROUTING_URL of NONE, there are safeguards to prevent us breaking a routing list when we do that.
image

To destroy,

# Think before you run this.
# clear; docker stop 2025box1; docker rm 2025box1; docker stop 2025box2; docker rm 2025box2; docker network rm AgMagic

…ead-only routing list. Closes BrentOzarULTD#3749.

Did not intend to add the read-only routing list check. However, the URL I gave mentions it as a requirement to make read-only routing work so there is no reason to not add it. Besides, it is pretty simple.
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.

sp_Blitz: warn if read-only routing for an Always On availability group is probably intended but broken

1 participant