Repository navigation
Lite: a server that just went down no longer holds a connection slot while its retries wait (#4722) - #4740
Merged
Conversation
…function (#4722) CreateConnectionAsync now hands its connect attempt to ExecuteThrottledWithRetryAsync, which takes the semaphore as a parameter and keeps today's shape: the throttle is taken once, around the whole RetryHelper loop. This gives the throttle tests a seam that needs no server and no shared static state. The credential lookup and the connection-string build now run before the throttle is taken. They are local reads, not a login.
…y backoff (#4722) CreateConnectionAsync held one of the seven connection-throttle slots across the whole RetryHelper loop. The dead-server errors (-2, -1, 2, 53, 10060, 10061) are on the transient list, so a server that had gone down since its last status check kept a slot through four connect timeouts and 1 + 2 + 4 seconds of backoff, for every collector that tried it. Seven of those at once stopped collection for every other server. ExecuteThrottledWithRetryAsync now takes the slot at the top of each attempt and gives it back in a finally, so the backoff between attempts holds none. The interactive sign-in lock is still taken first and held outside, and the attempt body (the device-code prompt and the open) stays inside the throttle, so a slot is held before any window shows. The new tests read the semaphore when RetryHelper logs "Retrying in": 6 of 7 free before this change, 7 of 7 after. They also pin that a successful, failed, cancelled or never-started attempt leaves the semaphore full, that each retried attempt runs holding a slot, and that CreateConnectionAsync takes the sign-in lock first and runs its attempts through the throttle.
Each connect attempt builds a SqlConnection of its own. When the open failed, the connection was left to the finalizer for non-interactive servers (the catch was filtered on the server being interactive), and for interactive ones too, since neither branch disposed it. A server that is down leaves up to four undisposed connections per collector per cycle. The catch is no longer filtered: the interactive branch still records a declined sign-in exactly as before, and every failed attempt then disposes its connection before rethrowing, as OpenAzureDatabaseConnectionAsync already does. A successful attempt still returns the open connection undisposed. A source pin in ConnectionThrottlePerAttemptTests covers the unfiltered catch, the dispose sitting at the catch block's own depth and before the rethrow, and the success path not disposing.
… so a prompt that fails to start leaves none undisposed (#4722)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4722
Why
Lite's
CreateConnectionAsyncheld one of the sevens_connectionThrottleslots across the wholeRetryHelperloop: up to four opens, each up to the connect timeout, plus 1 + 2 + 4 seconds of backoff between them. The dead-server errors (-2, -1, 2, 53, 10060, 10061) are on the transient list inPerformanceMonitor.Common/SqlErrorClassification.cs, so a server that had gone down since its last status check kept a slot through all of that, for every collector that tried it. Seven of those at once stopped collection for every server.This is the Lite side of #4710. Lite's scheduler already skips servers marked offline and it has no runtime to drop on a -2 timeout, so nothing else from that issue applies here.
What changes
Lite/Services/RemoteCollectorService.cs:internal static ExecuteThrottledWithRetryAsync<T>(SemaphoreSlim throttle, Func<Task<T>> attempt, ILogger? logger, string operationName, CancellationToken cancellationToken).CreateConnectionAsyncruns its connect attempt through it withs_connectionThrottle. The semaphore is a parameter so the tests use one of their own; the production one is process-wide and the suite runs classes in parallel.finally, so the backoffRetryHelperwaits out between attempts holds no slot. A wait for a slot that is cancelled releases nothing, because nothing was held.OpenAsync.s_connectionThrottleis reworded for the per-attempt hold.OpenAzureDatabaseConnectionAsyncalready does; before, each failed attempt left one undisposed, up to four per collector per cycle against a server that is down. Thecatchin the retry lambda is no longer filtered on the server being interactive: the interactive branch still records a declined sign-in exactly as before, and then every failed attempt disposes its connection before rethrowing. A successful attempt still returns the open connection undisposed.EntraDeviceCodeAuth.Beginthat throws leaves no connection behind.The change is four commits. The first extracts the function with today's shape (the throttle around the whole retry loop), so the new test could be run against the old behavior. The second is the throttle fix. The third disposes the connection of a failed attempt. The fourth moves the connection's creation below the device-code prompt.
Test plan
New
Lite.Tests/ConnectionThrottlePerAttemptTests.cs, 9 tests:TheSlotIsFree_WhileTheRetryBackoffWaits: a logger sees the "Retrying in" line, reads the semaphore'sCurrentCount, then cancels so the test skips the delay.EveryAttempt_RunsHoldingASlot: refused once with 10061, then opens; both attempts read 6 of 7 free from inside (one real second of backoff).ASuccessfulAttempt_HoldsASlotWhileItRuns_AndReleasesItOnReturn: 6 of 7 inside the attempt, 7 of 7 after. This one also fails a fix that stops taking the throttle at all.ACancelledAttempt_ReleasesItsSlot,APermanentFailure_ReleasesItsSlot_AndIsNotRetried,ACancelledWaitForASlot_ReleasesNothing(0 of 7 free while queued, 7 of 7 after the slots come back).CreateConnectionAsync_TakesTheSignInLockFirst_ThenRunsItsAttemptsThroughTheThrottle: a source pin for the wiring, the sign-in lock before the throttle and the device-code prompt inside it.AFailedOpen_DisposesItsConnection_ForEveryKindOfServer: a source pin over the code ofCreateConnectionAsync(comments and strings blanked). Thecatcharound the open is not filtered,connection.Dispose();sits at the catch block's own depth (not only inside the interactive branch) and before the rethrow, and the success path returnsconnectionwithout disposing it.TheTestException_IsATransientSqlError: checks the reflection-builtSqlExceptionreally carries the transient numbers, so the retry tests can't pass for the wrong reason.Results:
dotnet build Lite.Tests/Lite.Tests.csproj -c Debuganddotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: 0 warnings, 0 errors.TheSlotIsFree_WhileTheRetryBackoffWaits, with "the throttle read 6 of 7 free while RetryHelper waited out its backoff; a slot is held across the delay". The other 7 passed.AFailedOpen_DisposesItsConnection_ForEveryKindOfServer, on the catch clausecatch (Exception ex) when (isInteractiveServer)being filtered.dotnet buildof Lite, Lite.Tests and Darling.Tests: 0 warnings, 0 errors.RemoteCollectorService.cs(EntraDeviceCodeTests,CollectorGateSurfacePinTests,EmptyEnumerationNoteTests,RefreshCollectionGateTests,TimeHonestyRungTests,MeasurementContractCensusTests,LockTimeoutYieldTests,AzureMasterFallbackTests), after the disposal change: 141 passed.RemoteCollectorServiceCancellationCensusTests,CollectorStateContractTests,CollectorMeasurementSeamTests,LiteJobHistorySourceTests), after the disposal change: 48 run, 0 failed, 1 skipped.Lite.Testsat the throttle fix (before the disposal change): Total 5593, Failed 0, Skipped 0.Lite.Testsafter the disposal change: not re-run here; CI runs it.CHANGELOG
SECTION: Fixed
ENTRY:
REF:
[Lite: a server that just went down holds connection-throttle slots through its retry backoff #4722]: Lite: a server that just went down holds connection-throttle slots through its retry backoff #4722