Connect-DbaInstance - Give the cloned server ownership of its database connection - #10584
Connect-DbaInstance - Give the cloned server ownership of its database connection#10584andreasjordan wants to merge 1 commit into
Conversation
…e connection When an existing server object is passed in together with a different -Database, the connection context is copied and the database connection was then created with GetDatabaseConnection. That opens the connection on the intermediate copy and returns a different ConnectionContext, so the server object we hand back never owns the connection. Disconnect-DbaInstance can only reach the context of the server it is given, so nothing ever closed it. The session therefore stayed open for the life of the process, sitting in the target database and holding a shared lock on it. On model that is enough to make a later CREATE DATABASE on the same instance fail with "Could not obtain exclusive lock on database model", which showed up as an intermittent failure in whatever test file happened to run next. Setting DatabaseName on the copy keeps the connection with the context that the returned server owns, so Disconnect-DbaInstance closes it. This is also what the connection string paths of this command already do, and it does not reset StatementTimeout, so the save and restore around the old call is no longer needed. Measured against one instance, connecting to a database and disconnecting again: before, four sessions were opened and one closed, leaving three behind including one parked in the database. Now two are opened and the database one is closed again. (do Connect-DbaInstance) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
seems straightforward but there must have been a reason we did it so weirdly. at the same time, it does cause issues. im going to ask gpt to investigate why we likely did it and what the consequences are of the change. |
|
GPT Pro said on my behalf: I dug through the history because this code was weird enough that I didn't believe we did it accidentally. The weird code had real reasonsThe original
Then more fixes accumulated around it:
So no, we didn't do this for funsies. But this PR found a real bugThe ownership problem described in #10584 checks out. Basically: A → Copy to B → GetDatabaseConnection creates C → return C But SMO can open/use B while creating C. We then throw away our reference to B and return a Server containing C.
So B can sit there for the life of the process holding a database lock. That explains both the accumulating sessions and the bizarre later failures trying to get an exclusive lock on For normal dbatools connections, changing this to: is much cleaner. It also makes several old workarounds unnecessary: no premature connection, no lost One thing I think this PR currently breaksWe also support creating a Server from a raw Those SMO contexts have an explicit connection string. So please add a regression test for: I expect the second call to fail on the new CI doesn't catch this because we test SqlConnection → Server and separately Server → different Database, but not SqlConnection → Server → different Database. What I'd change before mergingKeep the new For explicit-connection-string contexts, construct the final database-specific context directly with the target database already in I'd also add coverage for Finally, I'd make the leak regression run several cycles. With pooling, a sleeping SQL session remaining is legitimate. What matters is that the session count stabilizes instead of growing every time, which your eight-cycle test already demonstrates really well. Bottom lineThe PR is fixing a real bug and I prefer the new design. I wouldn't revert to the old approach. I just want the raw |
When an existing server object is passed in together with a different
-Database, the connection context is copied and the database connection was then created withGetDatabaseConnection:GetDatabaseConnectionopens the connection on the intermediate copy and returns a differentConnectionContext, so the server object that is handed back never owns that connection.Disconnect-DbaInstancecan only reach the context of the server it is given, so nothing ever closed it.The session therefore stayed open for the life of the process, sitting in the target database and holding a shared
DATABASElock on it. Onmodelthat is enough to make a laterCREATE DATABASEon the same instance fail:which surfaced as an intermittent failure in whatever test file happened to run next, with no connection to the command that caused it.
Where the connection goes
Running the steps of the old code one at a time and counting sessions on the instance after each:
Step 3 opens the connection on the copy. Step 3 also reassigns
$connContext, which drops the only reference to the object that owns it. Step 6 shows the consequence: the server we return has nothing to close. Step 7 shows who did own it - and that disconnecting the copy is not a usable fix either, because that is the working connection.The change
Setting
DatabaseNameon the copy keeps the connection with the context that the returned server owns, soDisconnect-DbaInstancecloses it. This is also what the connection string paths of this command already do, so the server object path now behaves like the others.History of the line, and why each earlier fix still holds
The line has been touched four times, and none of the reasons are lost by this change:
ConnectionContext.Copy().GetDatabaseConnection($Database), next to a# TODO: Do we have to check if its the same database?GetDatabaseConnectionopens the connection and later property assignments would come too lateDatabaseNamedoes not open a connection - the trace above shows nothing opens until the first query. The assignment is kept in the same last position anywayGetDatabaseConnectionresetsStatementTimeoutStatementTimeoutis set earlier on the copy and simply stays.clones when using parameter StatementTimeoutcovers itGetDatabaseConnection($Database, $false), to force a non-pooled connection. Without it,Backup-DbaDatabasegot a cached connection to an already dropped database and the context change silently did not happen. Also added the warning whenCurrentDatabasedoes not match#9505 is the one worth care, because
$falseforced a non-pooled connection and settingDatabaseNamedoes not. It holds becauseDatabaseNameputsInitial Cataloginto the connection string, so the database is part of the pool key: a pooled connection then comes from that database's pool instead of being taken from the original database's pool and switched.Replaying the exact #9505 scenario against this change - create a database, connect with it as context, drop it, then ask for a clone on
master:And with pooling left on everywhere, which is the case
$falseused to opt out of:The test #9505 added,
clones when using Backup-DabInstace, is still in the suite and passes.Measured
The clearest way to see it is to repeat the call. Eight cycles of connect to a database from an existing server object, each followed by
Disconnect-DbaInstance, counting the module's sessions on the instance after every cycle:Before - sessions accumulate without bound, and every new one sits in the target database:
After - flat, and the same spids are reused every cycle:
That is the difference between an orphaned non-pooled connection, which nothing can ever reuse or close, and a pooled one that goes back to the pool on disconnect. The sessions that remain after the fix are the pool's: they stay
sleeping, they are reused by the next call, and their number does not grow. Connecting from a plain instance name behaves the same way in both versions, which is why that path never had the problem.At suite scale the same difference: the full 744 file run of 2026-08-15 had 69 files failing with
Timeout expired ... prior to obtaining a connection from the pool, and the same run with this fix had zero.Commands that read a per database view through
Invoke-DbaQuery -Databasestop leaking without being touched.Get-DbaDbQueryStoreOption -Database modelwent from three leaked sessions, one of them inmodel, to one leaked session and none inmodel.Tests
tests/Connect-DbaInstance.Tests.ps1gets a regression test that connects to a database, disconnects again and asserts the session count is back where it started. It was verified to fail against the old implementation (Expected 4, but got 5) and to be the only test that fails there.Run against SQL Server 2025, including the commands that use this path:
Connect-DbaInstanceInvoke-DbaQueryRemove-DbaDbDataGet-DbaUserPermissionGet-DbaDbRecoveryModelGet-DbaDbQueryStoreOptionDisconnect-DbaInstance,Remove-DbaDbAsymmetricKey,Remove-DbaDbCertificate,Remove-DbaDbEncryptionKey106 tests, no failures.
This text was created by Claude and reviewed by Andreas Jordan.
🤖 Generated with Claude Code