Skip to content

fix: Close the demoted anchor's SDK client when a re-anchor commits#744

Open
keelerm84 wants to merge 3 commits into
mk/sdk-2544/handler-fan-outfrom
mk/close-demoted-anchor-client
Open

fix: Close the demoted anchor's SDK client when a re-anchor commits#744
keelerm84 wants to merge 3 commits into
mk/sdk-2544/handler-fan-outfrom
mk/close-demoted-anchor-client

Conversation

@keelerm84

@keelerm84 keelerm84 commented Jul 9, 2026

Copy link
Copy Markdown
Member

A grace-demoted previous anchor stayed accepted with a future expiry, so
removeCredential never fired for it at rotation time. Its SDK client kept
its upstream stream open alongside the new anchor's client, and because
both clients share the handed-over store wrapper (which broadcasts every
Upsert), each flag update was sent twice to all connected downstream
clients until the demoted key's expiry finally closed the old client.

reanchor now closes and removes the previous anchor's client as soon as
CommitAnchor lands. Only the client goes: the demoted key's credential
mappings remain registered, so it still authenticates downstream
connections for the rest of its grace period. The refcounted store
wrapper survives the close because the new anchor's client holds it.

Consequence: re-promoting an in-grace key back to anchor now builds a
fresh client instead of reusing a retained one, since demotion tears the
old client down. Tests asserting the retained-client behavior were
updated to assert the new invariant (a single upstream client per
environment at all times).


Note

Medium Risk
Touches credential rotation and upstream client lifecycle in reanchor; behavior change is intentional but affects all SDK key rotations and offline is explicitly exempted.

Overview
Fixes double-broadcast of flag updates during SDK key rotation when the old anchor remains accepted on a grace expiry.

reanchor now closes and removes the previous anchor’s upstream SDK client immediately after a successful commitReanchor. The demoted key’s credential mappings stay so downstream auth still works until expiry; only the extra upstream stream is torn down so the shared store wrapper is not fed by two clients. Offline envs skip this teardown so the single file-data client keeps serving.

Re-promoting a grace-period key back to anchor builds a new client (the demoted client is already gone), not reuse of a retained client.

Tests and auto-config coverage were updated for the new invariant, plus an end-to-end test with real SDK clients asserting one patch per upstream update after rotation.

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

keelerm84 added 3 commits July 9, 2026 16:38
A grace-demoted previous anchor stayed accepted with a future expiry, so
removeCredential never fired for it at rotation time. Its SDK client kept
its upstream stream open alongside the new anchor's client, and because
both clients share the handed-over store wrapper (which broadcasts every
Upsert), each flag update was sent twice to all connected downstream
clients until the demoted key's expiry finally closed the old client.

reanchor now closes and removes the previous anchor's client as soon as
CommitAnchor lands. Only the client goes: the demoted key's credential
mappings remain registered, so it still authenticates downstream
connections for the rest of its grace period. The refcounted store
wrapper survives the close because the new anchor's client holds it.

Consequence: re-promoting an in-grace key back to anchor now builds a
fresh client instead of reusing a retained one, since demotion tears the
old client down. Tests asserting the retained-client behavior were
updated to assert the new invariant (a single upstream client per
environment at all times).
Exercises the real rotation path with real SDK clients against a fake
LaunchDarkly streaming service: an auto-config patch rotates the anchor
key while demoting the old key with a grace expiry, and a downstream SSE
client (authenticated with the old key) stays connected throughout.

Asserts the rotation opens a new upstream connection with the new key,
closes the demoted key's client once the new anchor commits, and
delivers exactly one copy of a subsequent upstream flag update to the
connected downstream client. Verified against the pre-fix code: it
fails there both on the unclosed client and, with that check removed,
on the duplicated downstream update.
The demoted-anchor client close added at re-anchor commit lacked the
offline guard that removeCredential has. An offline env has exactly one
file-data SDK client and the offline re-anchor branch builds no
replacement, so closing the previous anchor's client left the env with
no client at all: GetClient() returned nil, /status reported the env
disconnected, and with a persistent store the wrapper refcount hit zero
and tore down the backing store.

Mirror removeCredential: skip the client teardown when offline. The
offline re-anchor still commits the anchor; the single client keeps
serving across rotations. Found by multi-agent review (general, M1).
@keelerm84 keelerm84 marked this pull request as ready for review July 9, 2026 20:44
@keelerm84 keelerm84 requested a review from a team as a code owner July 9, 2026 20:44
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.

1 participant