Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
19d9c02
chore: adds fdv2 payload parsing and protocol handling
tanderson-ld Jan 12, 2026
fbea872
adding package info files and fixing package name issue
tanderson-ld Jan 12, 2026
adcaa0e
more checkstyle fixes
tanderson-ld Jan 12, 2026
f2b209d
chore: Add interfaces for synchronizer/initializer.
kinyoklion Jan 13, 2026
8c115cc
Merge branch 'main' into rlamb/add-fdv2-data-source-interfaces
kinyoklion Jan 13, 2026
de2fded
Revert version change
kinyoklion Jan 13, 2026
98d3b39
feat: Add FDv2 polling support.
kinyoklion Jan 13, 2026
da3c639
Merge remote-tracking branch 'origin' into rlamb/add-fdv2-data-source…
kinyoklion Jan 13, 2026
8fb88ed
WIP: Polling initializer/synchronizer.
kinyoklion Jan 14, 2026
da27015
Use updated internal lib.
kinyoklion Jan 14, 2026
aba46ef
Update comment
kinyoklion Jan 14, 2026
7401331
Add termination.
kinyoklion Jan 14, 2026
bba0cdc
Remove test file that isn't ready.
kinyoklion Jan 14, 2026
89bd017
Polling tests and some fixes.
kinyoklion Jan 14, 2026
228f3e6
Try pre block.
kinyoklion Jan 14, 2026
9469b23
Add streaming path.
kinyoklion Jan 14, 2026
9a450e6
Merge branch 'main' of github.com:launchdarkly/java-core into rlamb/a…
kinyoklion Jan 14, 2026
4b8313b
Use the DataStoreTypes.ChangeSet type for data source results.
kinyoklion Jan 14, 2026
31eb13e
Make iterable async queue package private.
kinyoklion Jan 14, 2026
4a2fe3b
Revert Version.java
kinyoklion Jan 14, 2026
3428591
Add comments to SelectorSource.
kinyoklion Jan 14, 2026
ff60216
Revert build.gradle.
kinyoklion Jan 14, 2026
e985f80
Update launchdarklyJavaSdkInternal version to 1.6.1
kinyoklion Jan 14, 2026
a956484
Move mermaid out of doc comment.
kinyoklion Jan 14, 2026
ff2376e
Merge branch 'rlamb/add-fdv2-data-source-interfaces' of github.com:la…
kinyoklion Jan 14, 2026
376bb1f
chore: Add streaming synchronizer.
kinyoklion Jan 14, 2026
194c30c
PR feedback.
kinyoklion Jan 14, 2026
707fe0e
Implement more shutdown logic.
kinyoklion Jan 14, 2026
cb79f5e
Change null check.
kinyoklion Jan 14, 2026
6702239
Merge branch 'rlamb/add-fdv2-data-source-interfaces' into rlamb/strea…
kinyoklion Jan 14, 2026
0aba424
chore: Implement streaming synchronizer.
kinyoklion Jan 14, 2026
91d2cb9
WIP
kinyoklion Jan 15, 2026
d610988
WIP
kinyoklion Jan 15, 2026
49c6008
Merge remote-tracking branch 'origin' into rlamb/streaming-synchronizer
kinyoklion Jan 15, 2026
b429eba
Basic streaming synchronizer.
kinyoklion Jan 15, 2026
278f670
Extend test coverage
kinyoklion Jan 15, 2026
84be62d
Add payload filter and more testing.
kinyoklion Jan 16, 2026
ec609a5
Add comments to FDv2 data source interfaces.
kinyoklion Jan 16, 2026
3461149
Remove extra blank lines
kinyoklion Jan 16, 2026
b32d3ca
Revert requestor change
kinyoklion Jan 16, 2026
97ac7c4
Remove leftover file.
kinyoklion Jan 16, 2026
7c84a68
Extend polling tests for INTERNAL_ERROR
kinyoklion Jan 16, 2026
9c43cbb
Handle close before start.
kinyoklion Jan 16, 2026
a383fc9
Threading and tests.
kinyoklion Jan 16, 2026
9c1c4c4
Update documentation.
kinyoklion Jan 16, 2026
dab1ef1
Merge branch 'main' of github.com:launchdarkly/java-core into rlamb/c…
kinyoklion Jan 16, 2026
68c490f
Merge branch 'rlamb/streaming-synchronizer' into rlamb/connect-data-s…
kinyoklion Jan 16, 2026
7bb6394
chore: Connect FDv2 Configuration.
kinyoklion Jan 17, 2026
ae39996
Fix data source selector related tests.
kinyoklion Jan 20, 2026
02c4c98
Refactor initializer/synchronizer builder context.
kinyoklion Jan 20, 2026
d4dff45
Merge branch 'main' into rlamb/streaming-synchronizer
kinyoklion Jan 20, 2026
776b4c6
Merge branch 'rlamb/streaming-synchronizer' into rlamb/connect-data-s…
kinyoklion Jan 20, 2026
d9dc5f0
Undo comment formatting.
kinyoklion Jan 20, 2026
20ec764
Simplify builder interfaces.
kinyoklion Jan 20, 2026
47e30b9
Individual imports.
kinyoklion Jan 20, 2026
dd1146b
Remove todo
kinyoklion Jan 20, 2026
38f90c2
Comment correction and minor code cleanup.
kinyoklion Jan 20, 2026
ee82077
Rename DataSourceBuildContext to DataSourceBuildInputs.
kinyoklion Jan 21, 2026
83b4217
Merge branch 'main' into rlamb/connect-data-source-configurations
kinyoklion Jan 21, 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
Copy link
Member Author

Choose a reason for hiding this comment

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

This file is a move because of package boundaries. Not sure how we want them to shake out.

The HttpConfig -> HttpProperties I think was the missing component, and moving that around created further complications.

Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.launchdarkly.sdk.server;

import com.launchdarkly.sdk.server.datasources.Initializer;
import com.launchdarkly.sdk.server.datasources.Synchronizer;
import com.launchdarkly.sdk.server.integrations.FDv2PollingInitializerBuilder;
import com.launchdarkly.sdk.server.integrations.FDv2PollingSynchronizerBuilder;
import com.launchdarkly.sdk.server.integrations.FDv2StreamingSynchronizerBuilder;
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints;
import com.launchdarkly.sdk.server.subsystems.DataSourceBuildInputs;

import java.net.URI;

import static com.launchdarkly.sdk.server.ComponentsImpl.toHttpProperties;

/**
* Components for use with the data system.
* <p>
* This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
* It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
* </p>
*/
public final class DataSystemComponents {

static class FDv2PollingInitializerBuilderImpl extends FDv2PollingInitializerBuilder {
@Override
public Initializer build(DataSourceBuildInputs context) {
ServiceEndpoints endpoints = serviceEndpointsOverride != null
? serviceEndpointsOverride
: context.getServiceEndpoints();
URI configuredBaseUri = StandardEndpoints.selectBaseUri(
endpoints.getPollingBaseUri(),
StandardEndpoints.DEFAULT_POLLING_BASE_URI,
"Polling",
context.getBaseLogger());

DefaultFDv2Requestor requestor = new DefaultFDv2Requestor(
toHttpProperties(context.getHttp()),
configuredBaseUri,
StandardEndpoints.FDV2_POLLING_REQUEST_PATH,
context.getBaseLogger());

return new PollingInitializerImpl(
requestor,
context.getBaseLogger(),
context.getSelectorSource()
);
}
}

static class FDv2PollingSynchronizerBuilderImpl extends FDv2PollingSynchronizerBuilder {
@Override
public Synchronizer build(DataSourceBuildInputs context) {
ServiceEndpoints endpoints = serviceEndpointsOverride != null
? serviceEndpointsOverride
: context.getServiceEndpoints();
URI configuredBaseUri = StandardEndpoints.selectBaseUri(
endpoints.getPollingBaseUri(),
StandardEndpoints.DEFAULT_POLLING_BASE_URI,
"Polling",
context.getBaseLogger());

DefaultFDv2Requestor requestor = new DefaultFDv2Requestor(
toHttpProperties(context.getHttp()),
configuredBaseUri,
StandardEndpoints.FDV2_POLLING_REQUEST_PATH,
context.getBaseLogger());

return new PollingSynchronizerImpl(
requestor,
context.getBaseLogger(),
context.getSelectorSource(),
context.getSharedExecutor(),
pollInterval
);
}
}

static class FDv2StreamingSynchronizerBuilderImpl extends FDv2StreamingSynchronizerBuilder {
@Override
public Synchronizer build(DataSourceBuildInputs context) {
ServiceEndpoints endpoints = serviceEndpointsOverride != null
? serviceEndpointsOverride
: context.getServiceEndpoints();
URI configuredBaseUri = StandardEndpoints.selectBaseUri(
endpoints.getStreamingBaseUri(),
StandardEndpoints.DEFAULT_STREAMING_BASE_URI,
"Streaming",
context.getBaseLogger());

return new StreamingSynchronizerImpl(
toHttpProperties(context.getHttp()),
configuredBaseUri,
StandardEndpoints.FDV2_STREAMING_REQUEST_PATH,
context.getBaseLogger(),
context.getSelectorSource(),
null,
initialReconnectDelay
);
}
}

private DataSystemComponents() {}

/**
* Get a builder for a polling initializer.
*
* @return the polling initializer builder
*/
public static FDv2PollingInitializerBuilder pollingInitializer() {
return new FDv2PollingInitializerBuilderImpl();
}

/**
* Get a builder for a polling synchronizer.
*
* @return the polling synchronizer builder
*/
public static FDv2PollingSynchronizerBuilder pollingSynchronizer() {
return new FDv2PollingSynchronizerBuilderImpl();
}

/**
* Get a builder for a streaming synchronizer.
*
* @return the streaming synchronizer builder
*/
public static FDv2StreamingSynchronizerBuilder streamingSynchronizer() {
return new FDv2StreamingSynchronizerBuilderImpl();
}

/**
* Get a builder for a FDv1 compatible polling data source.
* <p>
* This is intended for use as a fallback.
* </p>
*
* @return the FDv1 compatible polling data source builder
*/
public static PollingDataSourceBuilder fDv1Polling() {
return Components.pollingDataSource();
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

This was a mistake in the implementation. So both polling and streaming have this update.

Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
* Implementation of FDv2Requestor for polling feature flag data via FDv2 protocol.
*/
public class DefaultFDv2Requestor implements FDv2Requestor, Closeable {
private static final String VERSION_QUERY_PARAM = "version";
private static final String STATE_QUERY_PARAM = "state";
private static final String BASIS_QUERY_PARAM = "basis";

private final OkHttpClient httpClient;
private final URI pollingUri;
Expand Down Expand Up @@ -67,11 +66,7 @@ public CompletableFuture<FDv2PayloadResponse> Poll(Selector selector) {
URI requestUri = pollingUri;

if (!selector.isEmpty()) {
requestUri = HttpHelpers.addQueryParam(requestUri, VERSION_QUERY_PARAM, String.valueOf(selector.getVersion()));
}

if (selector.getState() != null && !selector.getState().isEmpty()) {
requestUri = HttpHelpers.addQueryParam(requestUri, STATE_QUERY_PARAM, selector.getState());
requestUri = HttpHelpers.addQueryParam(requestUri, BASIS_QUERY_PARAM, selector.getState());
}

logger.debug("Making FDv2 polling request to: {}", requestUri);
Expand Down
Copy link
Member Author

Choose a reason for hiding this comment

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

Still lots of work to do here. Outside the scope of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you intend to add tests in the subsequent PR?

Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.launchdarkly.sdk.server;

import com.google.common.collect.ImmutableList;
import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
import com.launchdarkly.sdk.server.datasources.Initializer;
import com.launchdarkly.sdk.server.datasources.Synchronizer;
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
import com.launchdarkly.sdk.server.subsystems.DataSource;
import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink;
import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSinkV2;

import java.io.Closeable;
import java.io.IOException;
Expand All @@ -18,10 +19,10 @@
import java.util.stream.Collectors;

class FDv2DataSource implements DataSource {
private final List<InitializerFactory> initializers;
private final List<DataSourceFactory<Initializer>> initializers;
private final List<SynchronizerFactoryWithState> synchronizers;

private final DataSourceUpdateSink dataSourceUpdates;
private final DataSourceUpdateSinkV2 dataSourceUpdates;

private final CompletableFuture<Boolean> startFuture = new CompletableFuture<>();
private final AtomicBoolean started = new AtomicBoolean(false);
Expand All @@ -46,12 +47,12 @@ public enum State {
Blocked
}

private final SynchronizerFactory factory;
private final DataSourceFactory<Synchronizer> factory;

private State state = State.Available;


public SynchronizerFactoryWithState(SynchronizerFactory factory) {
public SynchronizerFactoryWithState(DataSourceFactory<Synchronizer> factory) {
this.factory = factory;
}

Expand All @@ -68,19 +69,15 @@ public Synchronizer build() {
}
}

public interface InitializerFactory {
Initializer build();
}

public interface SynchronizerFactory {
Synchronizer build();
public interface DataSourceFactory<T> {
T build();
}


public FDv2DataSource(
List<InitializerFactory> initializers,
List<SynchronizerFactory> synchronizers,
DataSourceUpdateSink dataSourceUpdates
ImmutableList<DataSourceFactory<Initializer>> initializers,
ImmutableList<DataSourceFactory<Synchronizer>> synchronizers,
DataSourceUpdateSinkV2 dataSourceUpdates
) {
this.initializers = initializers;
this.synchronizers = synchronizers
Expand Down Expand Up @@ -116,6 +113,40 @@ private SynchronizerFactoryWithState getFirstAvailableSynchronizer() {
}
}

private void runInitializers() {
Copy link
Member Author

Choose a reason for hiding this comment

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

I moved this for better logical organization.

boolean anyDataReceived = false;
for (DataSourceFactory<Initializer> factory : initializers) {
try {
Initializer initializer = factory.build();
if (setActiveSource(initializer)) return;
FDv2SourceResult result = initializer.run().get();
switch (result.getResultType()) {
case CHANGE_SET:
dataSourceUpdates.apply(result.getChangeSet());
Copy link
Member Author

Choose a reason for hiding this comment

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

Actual change to the method. Apply the data.

Copy link

Choose a reason for hiding this comment

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

Ignored return value of apply causes incorrect initialization

Medium Severity

The return value of dataSourceUpdates.apply() is ignored in both runInitializers and runSynchronizers. The apply() method returns false when the store operation fails, but the code proceeds to set anyDataReceived = true and complete startFuture with true regardless. This causes the SDK to report successful initialization even when data failed to persist to the store, leading to an inconsistent state where isInitialized() returns true but the store lacks the expected data.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

I am going to ignore this for now, until we work on other FDv2DataSource updates.

anyDataReceived = true;
if (!result.getChangeSet().getSelector().isEmpty()) {
// We received data with a selector, so we end the initialization process.
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null);
startFuture.complete(true);
return;
}
break;
case STATUS:
// TODO: Implement.
break;
}
} catch (ExecutionException | InterruptedException | CancellationException e) {
// TODO: Log.
}
}
// We received data without a selector, and we have exhausted initializers, so we are going to
// consider ourselves initialized.
if (anyDataReceived) {
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null);
startFuture.complete(true);
}
}

private void runSynchronizers() {
SynchronizerFactoryWithState availableSynchronizer = getFirstAvailableSynchronizer();
// TODO: Add recovery handling. If there are no available synchronizers, but there are
Expand All @@ -130,7 +161,7 @@ private void runSynchronizers() {
FDv2SourceResult result = synchronizer.next().get();
switch (result.getResultType()) {
case CHANGE_SET:
// TODO: Apply to the store.
dataSourceUpdates.apply(result.getChangeSet());
// This could have been completed by any data source. But if it has not been completed before
// now, then we complete it.
startFuture.complete(true);
Expand Down Expand Up @@ -186,40 +217,6 @@ private boolean setActiveSource(Closeable synchronizer) {
return false;
}

private void runInitializers() {
boolean anyDataReceived = false;
for (InitializerFactory factory : initializers) {
try {
Initializer initializer = factory.build();
if (setActiveSource(initializer)) return;
FDv2SourceResult res = initializer.run().get();
switch (res.getResultType()) {
case CHANGE_SET:
// TODO: Apply to the store.
anyDataReceived = true;
if (!res.getChangeSet().getSelector().isEmpty()) {
// We received data with a selector, so we end the initialization process.
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null);
startFuture.complete(true);
return;
}
break;
case STATUS:
// TODO: Implement.
break;
}
} catch (ExecutionException | InterruptedException | CancellationException e) {
// TODO: Log.
}
}
// We received data without a selector, and we have exhausted initializers, so we are going to
// consider ourselves initialized.
if (anyDataReceived) {
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null);
startFuture.complete(true);
}
}

@Override
public Future<Void> start() {
if (!started.getAndSet(true)) {
Expand Down
Loading