Skip to content

test(sharedtest): make FakeLDClient.Close idempotent - #799

Draft
aaron-zeisler wants to merge 2 commits into
v8from
aaronz/fix-fake-client-double-close
Draft

test(sharedtest): make FakeLDClient.Close idempotent#799
aaron-zeisler wants to merge 2 commits into
v8from
aaronz/fix-fake-client-double-close

Conversation

@aaron-zeisler

@aaron-zeisler aaron-zeisler commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes FakeLDClient.Close() idempotent in the shared test harness, fixing an intermittent panic: close of closed channel that can take down an entire go test run.

Background

Close() closed CloseCh unconditionally whenever it was non-nil, with no guard against being called twice on the same instance. This was observed once as a flake in TestConcurrentKeysRAC_ArrayPatchAddsAndRemovesNonAnchorKeys on the feat/concurrent-keys branch; the affected FakeLDClient struct and Close() method are identical across v8, v9, and feat/concurrent-keys (the surrounding file differs on v9, which uses a different data-destination API, but the struct and this method are untouched).

I was not able to reproduce the panic (dozens of -race runs of the specific test and of the full relay package all passed), and I audited every production client.Close() call site in internal/relayenv/env_context_impl.go — they all delete the client from the map before closing it, or evict the previous reference before installing a new one, so I couldn't find a path that double-closes the same client reference. The actual second closer for the one observed panic remains unidentified.

Given that, this fix is intentionally defensive rather than a root-cause fix: it makes the fake tolerate a second Close() regardless of which caller triggers it, which also brings it in line with production semantics — the real SDK's own components (event_processor, streaming_data_source, Broadcaster) are already idempotent on Close() via the same sync.Once/no-op pattern this uses. If the panic recurs, the original stack trace would help pin down the actual second caller.

Changes

  • Guard the channel close with closeOnce sync.Once, matching the closeOnce idiom already used throughout this codebase (metrics, streams, autoconfig, bigsegments, filedata, events).
  • Add a doc comment on Close() stating the idempotency contract.
  • Add fake_client_test.go covering two sequential Close() calls and N concurrent callers under -race — this test reproduces the original panic if the guard is removed.

Close() closed CloseCh unconditionally whenever it was non-nil, so any
path that closed the same fake client twice (e.g. env teardown racing
a re-anchor that already closed the old client) panicked with "close
of closed channel" and took down the whole test binary. Guard the
close with sync.Once, matching the closeOnce pattern already used
elsewhere in the codebase (metrics, streams, autoconfig, bigsegments,
filedata, events) for the same purpose.
Adds a doc comment on Close() explaining why it must tolerate a
second call, plus a regression test that fails with the original
"close of closed channel" panic if the sync.Once guard is ever
removed. Also covers concurrent callers under -race, which a plain
bool guard would not.
@aaron-zeisler aaron-zeisler changed the title fix(sharedtest): make FakeLDClient.Close idempotent test(sharedtest): make FakeLDClient.Close idempotent Aug 6, 2026
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