Classify clock-skew invalid_client as retriable, not a deleted registration - #4694
Classify clock-skew invalid_client as retriable, not a deleted registration#4694canblmz1 wants to merge 3 commits into
Conversation
… deleted registration MessageListener.CreateSessionAsync and BrokerMessageListener.CreateSessionAsync both short-circuit on OAuth invalid_client before IsSessionCreationExceptionRetriable ever runs, printing "the runner registration has been deleted" and returning CreateSessionResult.Failure unconditionally. Runner.cs maps that Failure to ReturnCode.TerminatedError, so systemd never retries. A token request rejected purely because of local clock skew also comes back as invalid_client, with "Current server time is ..." in the exception message - the same sentinel IsSessionCreationExceptionRetriable already uses to classify and retry clock skew (30s x 30 min). The existing short-circuit ran first, so that retry path was unreachable for this case. Guard both invalid_client checks (the exception's own .Error, and the ValidateCredentialAsync fallback probe) on that sentinel. When present, skip the deleted-registration classification and fall through to the existing clock-skew retry path instead. Genuine deleted registrations (invalid_client without the sentinel) are unaffected - still an immediate Failure with the existing message. Fixes actions#4648. Tests: MessageListenerL0.CreateSession_ClockSkewInvalidClient_RetriesInsteadOfTerminating proves the skew case now retries and can succeed once the clock catches up, and fails against the pre-fix code (verified by temporarily reverting the source change and re-running). MessageListenerL0.CreateSession_InvalidClientWithoutClockSkew_StillTerminatesAsDeletedRegistration proves genuine deleted-registration handling is unchanged, in both states. Local branch only - not pushed, no PR opened.
There was a problem hiding this comment.
🟡 Changes recommended
The new clock-skew branch adds noisy/duplicative terminal output and BrokerMessageListener’s behavior change lacks equivalent targeted L0 regression coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a self-hosted runner boot-time failure mode where OAuth invalid_client errors caused by system clock skew were incorrectly treated as a deleted runner registration, leading to an immediate CreateSessionResult.Failure and a non-retrying service exit.
Changes:
- Updates
CreateSessionAsyncin bothMessageListenerandBrokerMessageListenerto bypass the “deleted registration”invalid_clientshort-circuit when the clock-skew sentinel ("Current server time is") is present, allowing the existing clock-skew retry path to run. - Adds L0 regression tests to ensure clock-skew
invalid_clientretries instead of terminating, while true deleted-registrationinvalid_clientstill terminates immediately.
File summaries
| File | Description |
|---|---|
| src/Runner.Listener/MessageListener.cs | Avoids misclassifying clock-skew invalid_client as deleted registration so existing retry logic can run. |
| src/Runner.Listener/BrokerMessageListener.cs | Mirrors the same clock-skew invalid_client handling change in the broker-based listener. |
| src/Test/L0/Listener/MessageListenerL0.cs | Adds regression coverage for clock-skew invalid_client retry vs. true deleted-registration termination. |
Review details
Suppressed comments (1)
src/Runner.Listener/BrokerMessageListener.cs:201
- The clock-skew invalid_client path currently writes a new terminal error line on every retry, even though IsSessionCreationExceptionRetriable already prints a dedicated clock-skew message and the outer retry loop prints its own reconnect message. To avoid noisy/duplicative user output (and to ensure we only treat true invalid_client as clock-skew), make the condition explicitly "invalid_client" + sentinel (case-insensitive, null-safe) and rely on the existing clock-skew messaging.
if (vssOAuthEx.Message.Contains("Current server time is"))
{
_term.WriteError($"Failed to create a session because of a clock-skewed invalid_client error: {vssOAuthEx.Message}");
Trace.Info("invalid_client with a clock-skew signature detected; deferring to clock-skew retry classification instead of treating the registration as deleted.");
}
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // A clock-skewed token request also comes back as "invalid_client", but its | ||
| // message carries "Current server time is ..." - the same sentinel | ||
| // IsSessionCreationExceptionRetriable checks below. That is not a deleted | ||
| // registration; the request was rejected only because this machine's clock | ||
| // hasn't caught up yet. Skip the deleted-registration classification and let |
There was a problem hiding this comment.
Fair catch - added mirrored coverage in BrokerMessageListenerL0 (CreateSession_ClockSkewInvalidClient_RetriesInsteadOfTerminating / CreateSession_InvalidClientWithoutClockSkew_StillTerminatesAsDeletedRegistration), same regression-proof method as the MessageListenerL0 tests (temporarily reverted BrokerMessageListener.cs alone and confirmed the skew test fails, then restored it). Full suite is 21/21 now.
…ssageListenerL0 Copilot review on actions#4694 flagged that the fix was applied to both MessageListener and BrokerMessageListener, but only MessageListenerL0 got new regression tests - the broker listener's identical guard had no direct coverage of its own, so a future change could regress just that path without either suite catching it. Adds the same two tests to BrokerMessageListenerL0, using _credMgr.LoadCredentials(true) (BrokerMessageListener loads _credsV2 via allowAuthUrlV2: true, which is what the invalid_client guard actually inspects) and _brokerServer.CreateSessionAsync instead of _runnerServer.CreateAgentSessionAsync. Verified the same way as the original MessageListenerL0 tests: temporarily reverted BrokerMessageListener.cs alone (git checkout HEAD~1 -- <file>) and reran - the clock-skew test failed (Expected: Success, Actual: Failure) while the deleted-registration test still passed in both states. Restored the fix; full MessageListenerL0 + BrokerMessageListenerL0 suite (21 tests) is green.
Copilot review on actions#4694 correctly flagged that the new WriteError fired on every retry attempt while skewed, duplicating IsSessionCreationExceptionRetriable's own clock-skew message a few lines later - and referenced 'invalid_client' without having actually confirmed .Error was that value at the point it printed. Removed the message entirely; the existing clock-skew retry path already tells the user what's happening. Kept a Trace.Info (internal log only, not user-facing) for diagnostics. Also made the sentinel check null-safe (vssOAuthEx.Message?.Contains(...) == true) per the same review. Left it case-sensitive, matching IsSessionCreationExceptionRetriable's own check exactly, since this guard is explicitly meant to recognize the same sentinel that check already uses - diverging case-sensitivity between the two would be a real (if narrow) way for them to disagree on the same input. Full MessageListenerL0 + BrokerMessageListenerL0 suite (21 tests) green.
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, keeps both listeners in sync, and is backed by targeted L0 regression tests covering both the fixed scenario and the unchanged terminal path.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary
Fixes #4648. When a self-hosted runner's system clock hasn't synchronized yet at boot,
CreateSessionAsync(bothMessageListenerandBrokerMessageListener) classifies the resulting OAuthinvalid_clientas a deleted registration, prints a misleading message, and returnsCreateSessionResult.Failureunconditionally - whichRunner.csmaps toReturnCode.TerminatedError, so systemd never retries and the runner stays offline until manually restarted.Root cause
invalid_clientshort-circuits toFailurebeforeIsSessionCreationExceptionRetriableever runs - in both listeners. That method already has a clock-skew retry path (checks for"Current server time is"in the exception message, retries every 30s for up to 30 minutes), but it's unreachable for this case because theinvalid_clientcheck above it returns first.A clock-skewed token request also comes back as
invalid_client, with"Current server time is ..."in the message - the exact sentinelIsSessionCreationExceptionRetriablealready checks for. It just never gets there.Fix
Minimal, no new retry policy: guard both
invalid_clientchecks (the exception's own.Error, and theValidateCredentialAsyncfallback probe) in each listener on that same sentinel. When present, skip the deleted-registration classification and fall through to the existing clock-skew retry path instead of returningFailure. A genuine deleted registration (invalid_clientwithout the sentinel) is unaffected - same message, same immediateFailure, unchanged.Kept both listeners in sync; the fix is structurally identical in each (
BrokerMessageListenerkeeps its existing!HostContext.AllowAuthMigrationouter condition untouched).Tests
Added to
MessageListenerL0:CreateSession_ClockSkewInvalidClient_RetriesInsteadOfTerminating-invalid_client+ clock-skew sentinel now retries (CreateAgentSessionAsynccalled twice: skew failure, then success) instead of terminating; no deleted-registration message is written.CreateSession_InvalidClientWithoutClockSkew_StillTerminatesAsDeletedRegistration- companion test proving a genuine deleted registration (no sentinel) still terminates immediately with exactly one attempt and the existing message - this fix must not weaken that path.Verified both are real regression coverage, not just passing incidentally: temporarily reverted the source change (
git stashon the two listener files only, tests kept) and reran - the skew test failed (Expected: Success, Actual: Failure) while the deleted-registration test still passed in both states. Restored the fix and reran the fullMessageListenerL0+BrokerMessageListenerL0suite (19 tests) - all green.Compatibility with #4330
Checked #4330's full diff against this change before opening this PR: it modifies
BrokerServer.ShouldRetryException(a different retry classifier, used for message polling, not session creation) and a separateRunnerNotFoundExceptionclassifier elsewhere inBrokerMessageListener.cs. It does not touchCreateSessionAsyncorIsSessionCreationExceptionRetriablein either listener - confirmed by checking out its branch and diffing the resulting file directly. No behavioral overlap; at most a trivial rebase if both land, since both touch the same file in different regions.Scope
src/Runner.Listener/MessageListener.cs,src/Runner.Listener/BrokerMessageListener.cs,src/Test/L0/Listener/MessageListenerL0.cs. No changes tosystemd/NTP handling, no changes toConnectivityAndDNSChecks(#4595), no new retry policy or configuration surface.