Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8e9510c
test: Wait for the ready event instead of asserting immediately
devin-ai-integration[bot] Aug 31, 2026
5a71aea
chore: trigger CI (1)
devin-ai-integration[bot] Aug 31, 2026
69cd472
chore: trigger CI (2)
devin-ai-integration[bot] Aug 31, 2026
89fc654
chore: trigger CI (3)
devin-ai-integration[bot] Aug 31, 2026
9d4f262
chore: trigger CI (4)
devin-ai-integration[bot] Aug 31, 2026
d0dfcc2
chore: trigger CI (5)
devin-ai-integration[bot] Aug 31, 2026
3ea239a
chore: trigger CI (6)
devin-ai-integration[bot] Aug 31, 2026
2778752
chore: trigger CI (7)
devin-ai-integration[bot] Aug 31, 2026
b8b696b
chore: trigger CI (8)
devin-ai-integration[bot] Aug 31, 2026
a1ab6ea
chore: trigger CI (9)
devin-ai-integration[bot] Aug 31, 2026
53c302f
chore: trigger CI (10)
devin-ai-integration[bot] Aug 31, 2026
cfa33ad
fix: Do not emit a redundant ready event during initialization
devin-ai-integration[bot] Aug 31, 2026
dde5812
chore: trigger CI (11)
devin-ai-integration[bot] Aug 31, 2026
f71563d
chore: trigger CI (12)
devin-ai-integration[bot] Aug 31, 2026
c3babcb
chore: trigger CI (13)
devin-ai-integration[bot] Aug 31, 2026
ae0d4e9
chore: trigger CI (14)
devin-ai-integration[bot] Aug 31, 2026
f41022d
chore: trigger CI (15)
devin-ai-integration[bot] Aug 31, 2026
e22c8b0
chore: trigger CI (16)
devin-ai-integration[bot] Aug 31, 2026
1d550f7
chore: trigger CI (17)
devin-ai-integration[bot] Aug 31, 2026
7559160
chore: trigger CI (18)
devin-ai-integration[bot] Aug 31, 2026
7e68dee
chore: trigger CI (19)
devin-ai-integration[bot] Aug 31, 2026
07e7993
chore: trigger CI (20)
devin-ai-integration[bot] Aug 31, 2026
811ad90
test: Cover ready events after a failed initialization
devin-ai-integration[bot] Aug 31, 2026
25e1801
chore: trigger CI (21)
devin-ai-integration[bot] Aug 31, 2026
2ded1db
chore: trigger CI (22)
devin-ai-integration[bot] Aug 31, 2026
e21ff22
chore: trigger CI (23)
devin-ai-integration[bot] Aug 31, 2026
3b811ed
chore: trigger CI (24)
devin-ai-integration[bot] Aug 31, 2026
e969437
chore: trigger CI (25)
devin-ai-integration[bot] Aug 31, 2026
bc6a773
chore: trigger CI (26)
devin-ai-integration[bot] Aug 31, 2026
1e7971d
chore: trigger CI (27)
devin-ai-integration[bot] Aug 31, 2026
aacfd26
chore: trigger CI (28)
devin-ai-integration[bot] Aug 31, 2026
5deabef
chore: trigger CI (29)
devin-ai-integration[bot] Aug 31, 2026
9220377
chore: trigger CI (30)
devin-ai-integration[bot] Aug 31, 2026
f8a6052
refactor: Read the initializing flag under the state lock
devin-ai-integration[bot] Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public String getName() {

private final Object stateLock = new Object();

private volatile boolean initializing = false;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Does a volatile handle all ordering concerns that may apply in this situation?

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.

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).


/**
* Create a provider with the specified SDK and default configuration.
* <p>
Expand Down Expand Up @@ -152,6 +154,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
setState(ProviderState.READY);
}

initializing = true;
var completer = new CompletableFuture<Boolean>();

client.getFlagTracker().addFlagChangeListener(detail -> {
Expand All @@ -164,11 +167,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
});

if (getState() == ProviderState.READY) {
initializing = false;
return;
}

handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
var successfullyInitialized = completer.get();
boolean successfullyInitialized;
try {
handleDataSourceStatus(client.getDataSourceStatusProvider().getStatus(), completer);
successfullyInitialized = completer.get();
} finally {
initializing = false;
}

if(!successfullyInitialized) {
throw new RuntimeException("Failed to initialize LaunchDarkly client.");
Expand Down Expand Up @@ -200,8 +209,12 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet
}

if (emit) {
// The OpenFeature SDK emits its own ready event when initialization succeeds.
boolean duringInitialization = initializing;
completer.complete(true);
emitProviderReady(ProviderEventDetails.builder().build());
if (!duringInitialization) {
emitProviderReady(ProviderEventDetails.builder().build());
}
}
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,29 @@ public void close() throws IOException {
}
}

class ControllableDataSource implements DataSource {
public Future<Void> start() {
return new CompletableFuture<>();
}

public boolean isInitialized() {
return false;
}

public void close() throws IOException {
}
}

class ControllableDataSourceFactory implements ComponentConfigurer<DataSource> {
final CompletableFuture<DataSourceUpdateSink> sink = new CompletableFuture<>();

@Override
public DataSource build(ClientContext clientContext) {
sink.complete(clientContext.getDataSourceUpdateSink());
return new ControllableDataSource();
}
}

class DelayedDataSourceFactory implements ComponentConfigurer<DataSource> {
private Duration startDelay;
private boolean willError;
Expand Down Expand Up @@ -219,14 +242,19 @@ public void itCanHandleClientThatIsNotInitializedImmediately() throws Exception
assertEquals(ProviderState.NOT_READY, provider.getState());

var readyCount = new AtomicInteger();
CompletableFuture<Boolean> gotReadyEvent = new CompletableFuture<>();

OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
readyCount.getAndIncrement();
gotReadyEvent.complete(true);
});

OpenFeatureAPI.getInstance().setProviderAndWait(provider);

assertEquals(ProviderState.READY, provider.getState());
assertTrue(gotReadyEvent.get(1000, TimeUnit.MILLISECONDS));

Thread.sleep(100);
assertEquals(1, readyCount.get());
}

Expand Down Expand Up @@ -260,6 +288,54 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E
assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS));
}

@Test
public void itEmitsReadyWhenTheDataSourceRecoversFromAFailedInitialization() throws Exception {
var dataSourceFactory = new ControllableDataSourceFactory();
var config = new LDConfig.Builder()
.startWait(Duration.ZERO)
.dataSource(dataSourceFactory)
.events(Components.noEvents())
.build();
var provider = new Provider("fake-key", config);
var sink = dataSourceFactory.sink.get(1000, TimeUnit.MILLISECONDS);

var readyCount = new AtomicInteger();
CompletableFuture<Boolean> gotReadyEvent = new CompletableFuture<>();
CompletableFuture<Boolean> gotErrorEvent = new CompletableFuture<>();

OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
readyCount.getAndIncrement();
gotReadyEvent.complete(true);
});

OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> {
gotErrorEvent.complete(true);
});

sink.updateStatus(DataSourceStatusProvider.State.OFF, new DataSourceStatusProvider.ErrorInfo(
DataSourceStatusProvider.ErrorKind.NETWORK_ERROR,
404,
"bad",
LocalDateTime.now().toInstant(ZoneOffset.UTC)));

GeneralError initializationError = null;
try {
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
} catch (GeneralError e) {
initializationError = e;
}

assertNotNull(initializationError);
assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS));

sink.updateStatus(DataSourceStatusProvider.State.VALID, null);

assertTrue(gotReadyEvent.get(1000, TimeUnit.MILLISECONDS));

Thread.sleep(100);
assertEquals(1, readyCount.get());
}

@Test
public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception {
var config = new LDConfig.Builder()
Expand Down
Loading