fix: Do not emit a redundant ready event during initialization - #63
fix: Do not emit a redundant ready event during initialization#63kinyoklion wants to merge 34 commits into
Conversation
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
|
I am concerned that this wouldn't detect if we emitted the ready event multiple times. |
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Good catch — and it was already emitting twice. I put the counter back with a short grace period after the awaited event, and it failed So this PR now also suppresses the provider's ready emit while |
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
|
|
||
| private final Object stateLock = new Object(); | ||
|
|
||
| private volatile boolean initializing = false; |
There was a problem hiding this comment.
Does a volatile handle all ordering concerns that may apply in this situation?
There was a problem hiding this comment.
Good catch — a bare volatile was not enough. It gave visibility for the flag itself, but the decision "did this VALID transition happen during initialize?" involves two pieces of state (initializing and state), and reading them at different times leaves a window: the flag could be cleared between the synchronized (stateLock) block that claims the ready transition and the read of initializing just after it, so the initial ready event could be emitted twice.
Latest commit makes the pair atomic instead: initializing is now a plain field always accessed under stateLock, and the "should we emit?" decision is made inside the same critical section that transitions state to READY. initialize writes the flag through setInitializing, which takes the same lock, so the flag write and the state read can't interleave.
Stress run after the change: 25/25 green under CPU load (6 busy loops).
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
Stops the provider from emitting a second
PROVIDER_READYevent during initialization, and makesLifeCycleTest.itCanHandleClientThatIsNotInitializedImmediatelywait for the event instead of racing it.initializewas still running, and the OpenFeature SDK emits its own ready event when initialization succeeds — consumers got two.expected: <1> but was: <2>25/25 times, which is how the double emit surfaced.initializeis in flight is suppressed.LifeCycleTestnow passes 25/25; before this branch it failed 9/25 withexpected: <1> but was: <0>.Implementation details
Requirements
Mechanism
initializesets aninitializingflag for the duration of the call. In theVALIDbranch ofhandleDataSourceStatusthe flag is read before completing the initialization future, so the initialize thread cannot clear it between the read and the emit:setProviderAndWaitalso returns before API-level handlers have run, since the OpenFeature SDK dispatches events on its own executor; the test now waits on aCompletableFuture(1s timeout) and then asserts the count after a short grace period so a duplicate emit is still caught.Alternatives rejected
Dropping the exact-count assertion from the test — it hides exactly the duplicate-emit bug this change fixes.
Testing
./gradlew test --tests '*LifeCycleTest*' --rerun-tasksrepeated 25x with six busy loops pinning the CPUs, and the full suite once: all green. Onmainthe same loop failed 9/25 (and 5/20 in an earlier round). CI on this branch was also re-triggered 11 times with empty commits, all green, before the provider change.Related
Found while investigating #32 (debug output for a flaky
itEmitsReadyEvents); that flake did not reproduce in ~75 runs and its debug prints are superseded by #59, so #32 looks closable.Link to Devin session: https://app.devin.ai/sessions/3c9b094de4f84a0f82ba266098e823e9
Open in Devin Desktop: https://app.devin.ai/desktop/session/3c9b094de4f84a0f82ba266098e823e9?variant=devin
Requested by: @kinyoklion
Note
Overview
Fixes double
PROVIDER_READYevents when the LaunchDarkly data source becomes valid whileinitializeis still running. The OpenFeature SDK already emits ready on successful initialization; the provider was also callingemitProviderReadyfrom the data-source status listener, so listeners could see two events.An
initializingflag wraps the wait ininitialize(cleared infinally). On transition toVALID, the provider still completes initialization and updates state, but skipsemitProviderReadywhileinitializingis true. Recoveries after init (e.g. stale/error back to valid) still emit ready as before.Tests:
itCanHandleClientThatIsNotInitializedImmediatelynow waits for the ready event and rechecks the count after a short delay to catch races. A new case uses a controllable data source to assert that failed init followed byVALIDstill produces exactly one ready event.Reviewed by Cursor Bugbot for commit f8a6052. Bugbot is set up for automated code reviews on this repo. Configure here.