Skip to content

Classify clock-skew invalid_client as retriable, not a deleted registration - #4694

Open
canblmz1 wants to merge 3 commits into
actions:mainfrom
canblmz1:fix/4648-clock-skew-invalid-client
Open

Classify clock-skew invalid_client as retriable, not a deleted registration#4694
canblmz1 wants to merge 3 commits into
actions:mainfrom
canblmz1:fix/4648-clock-skew-invalid-client

Conversation

@canblmz1

@canblmz1 canblmz1 commented Sep 9, 2026

Copy link
Copy Markdown

Summary

Fixes #4648. When a self-hosted runner's system clock hasn't synchronized yet at boot, CreateSessionAsync (both MessageListener and BrokerMessageListener) classifies the resulting OAuth invalid_client as a deleted registration, prints a misleading message, and returns CreateSessionResult.Failure unconditionally - which Runner.cs maps to ReturnCode.TerminatedError, so systemd never retries and the runner stays offline until manually restarted.

Root cause

invalid_client short-circuits to Failure before IsSessionCreationExceptionRetriable ever 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 the invalid_client check 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 sentinel IsSessionCreationExceptionRetriable already checks for. It just never gets there.

Fix

Minimal, no new retry policy: guard both invalid_client checks (the exception's own .Error, and the ValidateCredentialAsync fallback 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 returning Failure. A genuine deleted registration (invalid_client without the sentinel) is unaffected - same message, same immediate Failure, unchanged.

Kept both listeners in sync; the fix is structurally identical in each (BrokerMessageListener keeps its existing !HostContext.AllowAuthMigration outer condition untouched).

Tests

Added to MessageListenerL0:

  • CreateSession_ClockSkewInvalidClient_RetriesInsteadOfTerminating - invalid_client + clock-skew sentinel now retries (CreateAgentSessionAsync called 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 stash on 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 full MessageListenerL0 + BrokerMessageListenerL0 suite (19 tests) - all green.

dotnet test src/Test/Test.csproj -c Debug --filter "FullyQualifiedName~MessageListenerL0"

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 separate RunnerNotFoundException classifier elsewhere in BrokerMessageListener.cs. It does not touch CreateSessionAsync or IsSessionCreationExceptionRetriable in 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 to systemd/NTP handling, no changes to ConnectivityAndDNSChecks (#4595), no new retry policy or configuration surface.

… 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.
@canblmz1
canblmz1 requested a review from a team as a code owner September 9, 2026 12:05
Copilot AI lite review requested due to automatic review settings September 9, 2026 12:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 CreateSessionAsync in both MessageListener and BrokerMessageListener to bypass the “deleted registration” invalid_client short-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_client retries instead of terminating, while true deleted-registration invalid_client still 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.

Comment on lines +190 to +194
// 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Runner.Listener/MessageListener.cs Outdated
…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.
@canblmz1
canblmz1 requested a balanced review from Copilot September 9, 2026 12:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@canblmz1
canblmz1 requested a balanced review from Copilot September 9, 2026 12:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

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.

GitHub Actions self-hosted runner should handle system clock skew gracefully instead of exiting permanently

2 participants