Skip to content

feat: Defer the SDK anchor flip to an explicit CommitAnchor#738

Merged
keelerm84 merged 1 commit into
feat/concurrent-keysfrom
mk/sdk-2542/credential-deferred-flip
Jul 8, 2026
Merged

feat: Defer the SDK anchor flip to an explicit CommitAnchor#738
keelerm84 merged 1 commit into
feat/concurrent-keysfrom
mk/sdk-2542/credential-deferred-flip

Conversation

@keelerm84

@keelerm84 keelerm84 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Third slice carved out of #720 (the re-anchor mechanism, SDK-2542), split for reviewability. Introduces the deferred anchor-flip seam the synchronous re-anchor is built on. Sibling of #736 (store handover, merged) and #737 (relayenv refactors) off feat/concurrent-keys.

Rotator.Reconcile no longer flips the SDK anchor pointer when the anchor changes. It returns a ReconcileResult reporting the AnchorChange (previous -> new), and a new CommitAnchor method moves the pointer. This is the seam the re-anchor needs: it lets the caller build (and initialize) the new anchor's SDK client before the pointer moves, instead of flipping first and racing an async build.

Serialization (why this is safe under concurrency)

Deferring the flip means it is now a second lock acquisition (CommitAnchor) rather than done in place inside Reconcile. That would open a window where Reconcile has queued the new anchor's addition but the pointer has not moved yet -- and the per-env cleanup ticker (triggerCredentialChanges) could drain that addition, run addCredential with the anchor still on the old key, skip startSDKClient, and leave the env with no upstream client. To close it, a new reconcileMu serializes reconcileCredentials against the cleanup ticker: the Reconcile -> CommitAnchor -> drain sequence is atomic against the ticker, exactly as concurrent reconciles already were. reconcileCredentials drains via a lock-free drainCredentialChanges (not triggerCredentialChanges) to avoid re-entering reconcileMu.

Behavior-preserving, including under concurrency: by the time addCredential runs for the new anchor, AnchorKey() already names it, so its upstream client starts exactly as before. Verified with go test -race, go vet, and make lint.

Tests

  • Rotator unit tests: Reconcile reports the change but leaves the pointer until CommitAnchor, and signals nil when unchanged.
  • Serialization regression: the cleanup ticker's triggerCredentialChanges blocks while a reconcile holds reconcileMu, then proceeds once released.

Deliberately out of scope (they land with the synchronous re-anchor, #739)

Stripping the new anchor from additions, rollback (RevertAnchorChange), and the mobile-primary repoint -- each depends on the synchronous client build to be correct, so they are not in this PR.

Split sequence

PR 3 of 4: store handover (#736, merged) -> relayenv refactors (#737) -> credential deferred-flip (this PR) -> synchronous re-anchor core (#739).


Note

Medium Risk
Touches credential rotation and per-env upstream client startup under concurrency; behavior is intended to stay equivalent when CommitAnchor runs before drain, but the new mutex and API contract are on a critical path for SDK key re-anchoring.

Overview
Introduces a deferred SDK anchor flip so callers can move the upstream anchor only when ready (e.g. after building the new client), instead of updating the pointer inside Rotator.Reconcile.

Credential rotator: Reconcile now returns ReconcileResult with an optional AnchorChange (previous → new anchor) and no longer assigns anchorKey in reconcileSDKKeys. Callers apply the transition with new CommitAnchor. Accepted-set diffs and queued additions/expirations behave as before.

Relay environment: reconcileCredentials runs ReconcileCommitAnchor (when needed) → drainCredentialChanges under new reconcileMu, so that sequence cannot interleave with the cleanup ticker’s triggerCredentialChanges. Draining is split into drainCredentialChanges to avoid re-entering the mutex from reconcile. Immediate CommitAnchor before drain preserves today’s behavior: when addCredential runs for the new anchor, AnchorKey() already matches it.

Tests: Rotator tests cover deferred flip, nil AnchorChange when unchanged, and updated flows that call CommitAnchor. A relayenv regression asserts the ticker blocks on reconcileMu while reconcile holds it.

Reviewed by Cursor Bugbot for commit 6f7b4fb. Bugbot is set up for automated code reviews on this repo. Configure here.

Reconcile no longer flips the rotator's SDK anchor pointer when the anchor
changes. It returns a ReconcileResult reporting the AnchorChange, and a new
CommitAnchor method moves the pointer -- the seam the synchronous re-anchor
needs to build the new anchor's client before the pointer moves.

Because the flip is now a second lock acquisition (CommitAnchor) rather than
done in place inside Reconcile, it must stay atomic with the addition Reconcile
queues. A new reconcileMu serializes reconcileCredentials against the cleanup
ticker's triggerCredentialChanges, so the ticker cannot drain the new anchor's
queued addition in the window before CommitAnchor -- which would otherwise run
addCredential with the anchor still on the old key, skip startSDKClient, and
leave the env with no upstream client. reconcileCredentials commits and drains
under reconcileMu; the drain body is extracted to drainCredentialChanges
(lock-free, caller holds reconcileMu) to avoid re-entrancy.

Behavior-preserving, including under concurrency. The additions-stripping,
rollback, and mobile-primary repoint that build on this seam land with the
synchronous re-anchor itself.

Tests: rotator unit tests for the deferred-flip contract; a serialization
regression asserting the ticker blocks while a reconcile holds reconcileMu.
@keelerm84 keelerm84 force-pushed the mk/sdk-2542/credential-deferred-flip branch from 0c4bb5a to 6f7b4fb Compare July 7, 2026 16:34
@keelerm84 keelerm84 marked this pull request as ready for review July 7, 2026 20:40
@keelerm84 keelerm84 requested a review from a team as a code owner July 7, 2026 20:40
@keelerm84 keelerm84 merged commit 3399a38 into feat/concurrent-keys Jul 8, 2026
18 checks passed
@keelerm84 keelerm84 deleted the mk/sdk-2542/credential-deferred-flip branch July 8, 2026 12:15
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.

2 participants