Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -4,6 +4,7 @@
import com.launchdarkly.sdk.android.integrations.CacheInitializerEntry;
import com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder;
import com.launchdarkly.sdk.android.integrations.DataSystemBuilder;
import com.launchdarkly.sdk.android.integrations.FDv1FallbackSynchronizerEntry;
import com.launchdarkly.sdk.android.integrations.PollingInitializerEntry;
import com.launchdarkly.sdk.android.integrations.PollingSynchronizerEntry;
import com.launchdarkly.sdk.android.integrations.StreamingSynchronizerEntry;
Expand Down Expand Up @@ -150,6 +151,14 @@ public Synchronizer build(DataSourceBuildInputs inputs) {
static final class FDv1PollingSynchronizerBuilderImpl implements DataSourceBuilder<Synchronizer> {

protected int pollIntervalMillis = LDConfig.DEFAULT_POLL_INTERVAL_MILLIS;
private ServiceEndpoints serviceEndpointsOverride;

static FDv1PollingSynchronizerBuilderImpl fromEntry(FDv1FallbackSynchronizerEntry entry) {
FDv1PollingSynchronizerBuilderImpl b = new FDv1PollingSynchronizerBuilderImpl();
b.pollIntervalMillis = entry.getPollIntervalMillis();
b.serviceEndpointsOverride = entry.getServiceEndpointsOverride();
return b;
}

public FDv1PollingSynchronizerBuilderImpl pollIntervalMillis(int pollIntervalMillis) {
this.pollIntervalMillis = pollIntervalMillis <= LDConfig.DEFAULT_POLL_INTERVAL_MILLIS ?
Expand All @@ -162,8 +171,15 @@ public Synchronizer build(DataSourceBuildInputs inputs) {
// The FDv1 endpoints have no POST variant. When usePost is enabled, the fetcher
// sends the context in the request body with REPORT so it stays out of the
// request URI. useReport has no effect inside the data system.
ServiceEndpoints endpoints = serviceEndpointsOverride != null
? serviceEndpointsOverride
: inputs.getServiceEndpoints();
URI pollingBase = StandardEndpoints.selectBaseUri(
endpoints.getPollingBaseUri(),
StandardEndpoints.DEFAULT_POLLING_BASE_URI,
"polling", inputs.getBaseLogger());
FeatureFetcher fetcher = new HttpFeatureFlagFetcher(
inputs.getServiceEndpoints().getPollingBaseUri(),
pollingBase,
inputs.isEvaluationReasons(),
inputs.getHttp().shouldUsePost(),
LDUtil.makeHttpProperties(inputs.getHttp()),
Expand Down Expand Up @@ -243,6 +259,20 @@ public static StreamingSynchronizerEntry streamingSynchronizer() {
return new StreamingSynchronizerEntry();
}

/**
* Returns an entry for the FDv1 fallback synchronizer of a connection mode.
* <p>
* The SDK switches to the FDv1 fallback synchronizer when LaunchDarkly directs it to
* fall back to FDv1. Pass the entry to
* {@link ConnectionModeBuilder#fdv1FallbackSynchronizer(FDv1FallbackSynchronizerEntry)}
* to set its poll interval or service endpoints for that mode.
*
* @return an FDv1 fallback synchronizer entry
*/
public static FDv1FallbackSynchronizerEntry fdv1FallbackSynchronizer() {
return new FDv1FallbackSynchronizerEntry();
}

/**
* Produces the default mode table used by {@link DataSystemBuilder#buildModeTable}.
* Defined here (rather than in {@code DataSystemBuilder}) because the FDv1 fallback
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.launchdarkly.sdk.android;

import com.launchdarkly.sdk.android.integrations.CacheInitializerEntry;
import com.launchdarkly.sdk.android.integrations.FDv1FallbackSynchronizerEntry;
import com.launchdarkly.sdk.android.integrations.InitializerEntry;
import com.launchdarkly.sdk.android.integrations.PollingInitializerEntry;
import com.launchdarkly.sdk.android.integrations.PollingSynchronizerEntry;
Expand Down Expand Up @@ -46,6 +47,11 @@ public static DataSourceBuilder<Synchronizer> toSynchronizerBuilder(Synchronizer
throw new IllegalArgumentException("Unsupported SynchronizerEntry: " + entry.getClass().getName());
}

public static DataSourceBuilder<Synchronizer> toFdv1FallbackSynchronizerBuilder(
FDv1FallbackSynchronizerEntry entry) {
return DataSystemComponents.FDv1PollingSynchronizerBuilderImpl.fromEntry(entry);
}

public static List<DataSourceBuilder<Initializer>> toInitializerBuilders(List<InitializerEntry> entries) {
List<DataSourceBuilder<Initializer>> out = new ArrayList<>(entries.size());
for (InitializerEntry entry : entries) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.launchdarkly.sdk.android.integrations;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.launchdarkly.sdk.android.ConnectionMode;
import com.launchdarkly.sdk.android.DataSystemComponents;
Expand All @@ -25,6 +26,12 @@
* up to date after initialization. The SDK uses the first synchronizer and falls
* back to subsequent ones if it encounters errors.
* <p>
* The <b>FDv1 fallback synchronizer</b> is an FDv1 polling synchronizer that the SDK
* switches to when LaunchDarkly directs it to fall back to FDv1. A customized mode keeps
* the SDK's default FDv1 fallback for that mode unless you configure it with
* {@link #fdv1FallbackSynchronizer(FDv1FallbackSynchronizerEntry)} or remove it with
* {@link #disableFdv1Fallback()}.
* <p>
* Obtain an instance from {@link com.launchdarkly.sdk.android.DataSystemComponents#customMode()},
* configure it, and pass it to
* {@link DataSystemBuilder#customizeConnectionMode(ConnectionMode, ConnectionModeBuilder)}:
Expand All @@ -43,6 +50,9 @@ public class ConnectionModeBuilder {

private final List<InitializerEntry> initializerEntries = new ArrayList<>();
private final List<SynchronizerEntry> synchronizerEntries = new ArrayList<>();
@Nullable
private FDv1FallbackSynchronizerEntry fdv1FallbackSynchronizerEntry;
private boolean fdv1FallbackDisabled;

/**
* Sets the initializers for this connection mode.
Expand Down Expand Up @@ -90,6 +100,43 @@ public final ConnectionModeBuilder synchronizers(@NonNull SynchronizerEntry... s
return this;
}

/**
* Configures the FDv1 fallback synchronizer for this connection mode, replacing the
* SDK's default for the mode. This undoes any earlier call to {@link #disableFdv1Fallback()}.
* <p>
* Use {@link DataSystemComponents#fdv1FallbackSynchronizer()} to obtain an entry:
* <pre><code>
* builder.fdv1FallbackSynchronizer(
* DataSystemComponents.fdv1FallbackSynchronizer()
* .pollIntervalMillis(600_000))
* </code></pre>
*
* @param fdv1FallbackSynchronizer the FDv1 fallback synchronizer entry
* @return this builder
*/
public ConnectionModeBuilder fdv1FallbackSynchronizer(
@NonNull FDv1FallbackSynchronizerEntry fdv1FallbackSynchronizer) {
this.fdv1FallbackSynchronizerEntry = fdv1FallbackSynchronizer;
this.fdv1FallbackDisabled = false;
return this;
}

/**
* Removes the FDv1 fallback synchronizer from this connection mode, so the SDK does not
* fall back to FDv1 in this mode. This undoes any earlier call to
* {@link #fdv1FallbackSynchronizer(FDv1FallbackSynchronizerEntry)}.
* <p>
* If you neither call this method nor configure an FDv1 fallback synchronizer, the mode
* keeps the SDK's default FDv1 fallback.
*
* @return this builder
*/
public ConnectionModeBuilder disableFdv1Fallback() {
this.fdv1FallbackSynchronizerEntry = null;
this.fdv1FallbackDisabled = true;
return this;
}

/**
* Returns the configured initializer entries as an unmodifiable list.
*
Expand All @@ -109,4 +156,23 @@ public List<InitializerEntry> getInitializerEntries() {
public List<SynchronizerEntry> getSynchronizerEntries() {
return Collections.unmodifiableList(synchronizerEntries);
}

/**
* Returns the configured FDv1 fallback synchronizer entry, or null if none was configured.
*
* @return the FDv1 fallback synchronizer entry, or null
*/
@Nullable
public FDv1FallbackSynchronizerEntry getFdv1FallbackSynchronizerEntry() {
return fdv1FallbackSynchronizerEntry;
}

/**
* Returns true if {@link #disableFdv1Fallback()} removed the FDv1 fallback from this mode.
*
* @return true if the FDv1 fallback is disabled for this mode
*/
public boolean isFdv1FallbackDisabled() {
return fdv1FallbackDisabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,27 @@ public Map<ConnectionMode, ModeDefinition> buildModeTable(boolean disableBackgro
for (Map.Entry<ConnectionMode, ConnectionModeBuilder> entry : connectionModeOverrides.entrySet()) {
ConnectionModeBuilder cmb = entry.getValue();

// Carry forward the default FDv1 fallback synchronizer only when the
// override supplies at least one initializer or synchronizer. An entirely
// empty override means "do nothing in this mode" (like OFFLINE), so there is
// no synchronizer that could receive the x-ld-fd-fallback header and the
// fallback slot would never be reached by FDv2DataSource.runSynchronizers().
// Give the mode an FDv1 fallback synchronizer only when the override supplies
// at least one initializer or synchronizer. An entirely empty override means
// "do nothing in this mode" (like OFFLINE), so there is no synchronizer that
// could receive the x-ld-fd-fallback header and the fallback slot would never
// be reached by FDv2DataSource.runSynchronizers(). Otherwise use the
// override's FDv1 fallback entry, or the default for the mode if it configures
// none, and no fallback if it disables the FDv1 fallback.
DataSourceBuilder<Synchronizer> fdv1FallbackSynchronizer = null;
if (!cmb.getInitializerEntries().isEmpty() || !cmb.getSynchronizerEntries().isEmpty()) {
ModeDefinition defaultForMode = table.get(entry.getKey());
fdv1FallbackSynchronizer = defaultForMode != null
? defaultForMode.getFdv1FallbackSynchronizer()
: null;
boolean hasDataSources =
!cmb.getInitializerEntries().isEmpty() || !cmb.getSynchronizerEntries().isEmpty();
if (hasDataSources && !cmb.isFdv1FallbackDisabled()) {
FDv1FallbackSynchronizerEntry fdv1FallbackEntry = cmb.getFdv1FallbackSynchronizerEntry();
if (fdv1FallbackEntry != null) {
fdv1FallbackSynchronizer =
FDv2EntryConverter.toFdv1FallbackSynchronizerBuilder(fdv1FallbackEntry);
} else {
ModeDefinition defaultForMode = table.get(entry.getKey());
fdv1FallbackSynchronizer = defaultForMode != null
? defaultForMode.getFdv1FallbackSynchronizer()
: null;
}
}

List<DataSourceBuilder<Initializer>> initializerBuilders =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.launchdarkly.sdk.android.integrations;

import com.launchdarkly.sdk.android.LDConfig;
import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints;

import androidx.annotation.Nullable;

/**
* Entry for the FDv1 fallback synchronizer of a connection mode.
* <p>
* The SDK switches a mode to this FDv1 polling synchronizer when LaunchDarkly directs it
* to fall back to FDv1. It is not one of the mode's synchronizers; set it with
* {@link ConnectionModeBuilder#fdv1FallbackSynchronizer(FDv1FallbackSynchronizerEntry)}.
* <p>
* Early access; not stable.
*/
public final class FDv1FallbackSynchronizerEntry {

private int pollIntervalMillis = LDConfig.DEFAULT_POLL_INTERVAL_MILLIS;

@Nullable
private ServiceEndpoints serviceEndpointsOverride;

public FDv1FallbackSynchronizerEntry() {
}

/**
* Sets the poll interval of the FDv1 fallback synchronizer. The default and the minimum
* are {@link LDConfig#DEFAULT_POLL_INTERVAL_MILLIS}.
*
* @param pollIntervalMillis the poll interval in milliseconds
* @return this entry
*/
public FDv1FallbackSynchronizerEntry pollIntervalMillis(int pollIntervalMillis) {
this.pollIntervalMillis = Math.max(pollIntervalMillis, LDConfig.DEFAULT_POLL_INTERVAL_MILLIS);
return this;
}

/**
* Overrides service endpoints for the FDv1 fallback synchronizer, or {@code null} to use
* the SDK's polling endpoint.
*
* @param endpoints the service endpoints, or null
* @return this entry
*/
public FDv1FallbackSynchronizerEntry serviceEndpointsOverride(@Nullable ServiceEndpoints endpoints) {
this.serviceEndpointsOverride = endpoints;
return this;
}

/**
* Overrides service endpoints for the FDv1 fallback synchronizer using a builder.
*
* @param serviceEndpointsBuilder the service endpoints builder
* @return this entry
*/
public FDv1FallbackSynchronizerEntry serviceEndpointsOverride(ServiceEndpointsBuilder serviceEndpointsBuilder) {
this.serviceEndpointsOverride = serviceEndpointsBuilder.createServiceEndpoints();
return this;
}

public int getPollIntervalMillis() {
return pollIntervalMillis;
}

@Nullable
public ServiceEndpoints getServiceEndpointsOverride() {
return serviceEndpointsOverride;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,37 @@ public void customizeConnectionMode_nullsFdv1FallbackWhenModeIsEmpty() {
assertNull(streaming.getFdv1FallbackSynchronizer());
}

@Test
public void customizeConnectionMode_disableFdv1FallbackRemovesFallbackFromThatModeOnly() {
DataSystemBuilder b = Components.dataSystem()
.customizeConnectionMode(
ConnectionMode.STREAMING,
DataSystemComponents.customMode()
.synchronizers(DataSystemComponents.streamingSynchronizer())
.disableFdv1Fallback())
.customizeConnectionMode(
ConnectionMode.POLLING,
DataSystemComponents.customMode()
.synchronizers(DataSystemComponents.pollingSynchronizer()));
Map<ConnectionMode, ModeDefinition> table = b.buildModeTable(false);
assertNull(table.get(ConnectionMode.STREAMING).getFdv1FallbackSynchronizer());
assertNotNull(table.get(ConnectionMode.POLLING).getFdv1FallbackSynchronizer());
assertNotNull(table.get(ConnectionMode.BACKGROUND).getFdv1FallbackSynchronizer());
}

@Test
public void customizeConnectionMode_configuredFdv1FallbackAppliesToModeWithoutDefault() {
// ONE_SHOT has no FDv1 fallback by default.
DataSystemBuilder b = Components.dataSystem()
.customizeConnectionMode(
ConnectionMode.ONE_SHOT,
DataSystemComponents.customMode()
.initializers(DataSystemComponents.pollingInitializer())
.fdv1FallbackSynchronizer(DataSystemComponents.fdv1FallbackSynchronizer()));
Map<ConnectionMode, ModeDefinition> table = b.buildModeTable(false);
assertNotNull(table.get(ConnectionMode.ONE_SHOT).getFdv1FallbackSynchronizer());
}

@Test
public void getConnectionModeOverrides_isUnmodifiable() {
DataSystemBuilder b = Components.dataSystem();
Expand Down
Loading
Loading