diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java index 7e6c5ec1d..a77a1b74c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java @@ -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; @@ -150,6 +151,14 @@ public Synchronizer build(DataSourceBuildInputs inputs) { static final class FDv1PollingSynchronizerBuilderImpl implements DataSourceBuilder { 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 ? @@ -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()), @@ -243,6 +259,20 @@ public static StreamingSynchronizerEntry streamingSynchronizer() { return new StreamingSynchronizerEntry(); } + /** + * Returns an entry for the FDv1 fallback synchronizer of a connection mode. + *

+ * 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 diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2EntryConverter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2EntryConverter.java index 07a72e7b8..30fd307b9 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2EntryConverter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2EntryConverter.java @@ -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; @@ -46,6 +47,11 @@ public static DataSourceBuilder toSynchronizerBuilder(Synchronizer throw new IllegalArgumentException("Unsupported SynchronizerEntry: " + entry.getClass().getName()); } + public static DataSourceBuilder toFdv1FallbackSynchronizerBuilder( + FDv1FallbackSynchronizerEntry entry) { + return DataSystemComponents.FDv1PollingSynchronizerBuilderImpl.fromEntry(entry); + } + public static List> toInitializerBuilders(List entries) { List> out = new ArrayList<>(entries.size()); for (InitializerEntry entry : entries) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java index cc9755cb0..d92b2b9bf 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java @@ -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; @@ -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. *

+ * The FDv1 fallback synchronizer 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()}. + *

* Obtain an instance from {@link com.launchdarkly.sdk.android.DataSystemComponents#customMode()}, * configure it, and pass it to * {@link DataSystemBuilder#customizeConnectionMode(ConnectionMode, ConnectionModeBuilder)}: @@ -43,6 +50,9 @@ public class ConnectionModeBuilder { private final List initializerEntries = new ArrayList<>(); private final List synchronizerEntries = new ArrayList<>(); + @Nullable + private FDv1FallbackSynchronizerEntry fdv1FallbackSynchronizerEntry; + private boolean fdv1FallbackDisabled; /** * Sets the initializers for this connection mode. @@ -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()}. + *

+ * Use {@link DataSystemComponents#fdv1FallbackSynchronizer()} to obtain an entry: + *


+     *     builder.fdv1FallbackSynchronizer(
+     *         DataSystemComponents.fdv1FallbackSynchronizer()
+     *             .pollIntervalMillis(600_000))
+     * 
+ * + * @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)}. + *

+ * 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. * @@ -109,4 +156,23 @@ public List getInitializerEntries() { public List 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; + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java index a96e96a4f..59e53aac5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java @@ -248,17 +248,27 @@ public Map buildModeTable(boolean disableBackgro for (Map.Entry 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 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> initializerBuilders = diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/FDv1FallbackSynchronizerEntry.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/FDv1FallbackSynchronizerEntry.java new file mode 100644 index 000000000..76c5d7156 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/FDv1FallbackSynchronizerEntry.java @@ -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. + *

+ * 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)}. + *

+ * 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; + } +} diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java index 0195ec4c1..ae7280e2c 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java @@ -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 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 table = b.buildModeTable(false); + assertNotNull(table.get(ConnectionMode.ONE_SHOT).getFdv1FallbackSynchronizer()); + } + @Test public void getConnectionModeOverrides_isUnmodifiable() { DataSystemBuilder b = Components.dataSystem(); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java index ed30edac1..278df9d7f 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java @@ -18,8 +18,8 @@ import org.junit.rules.Timeout; import java.util.Collections; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; @@ -29,7 +29,8 @@ /** * Tests the request wiring of the data source builder implementations in * {@link DataSystemComponents}: which HTTP method, path, and body each FDv2 - * component and the FDv1 fallback synchronizer use for a given HTTP configuration. + * component and the FDv1 fallback synchronizer use for a given HTTP configuration, and + * which endpoint and poll interval a mode's configured FDv1 fallback synchronizer uses. */ public class DataSystemComponentsTest { @@ -39,7 +40,7 @@ public class DataSystemComponentsTest { @Rule public TemporaryFolder cacheDir = new TemporaryFolder(); - private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + private final FixedDelayRecordingExecutor executor = new FixedDelayRecordingExecutor(); private static final LDContext CONTEXT = LDContext.create("test-context-key"); private static final LDLogger LOGGER = LDLogger.none(); @@ -60,6 +61,22 @@ public void tearDown() { executor.shutdownNow(); } + /** Records the delay of the last fixed-delay task scheduled on it. */ + private static final class FixedDelayRecordingExecutor extends ScheduledThreadPoolExecutor { + volatile long lastFixedDelayMillis = -1; + + FixedDelayRecordingExecutor() { + super(2); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + lastFixedDelayMillis = unit.toMillis(delay); + return super.scheduleWithFixedDelay(command, initialDelay, delay, unit); + } + } + private static HttpConfiguration httpConfig(boolean useReport, boolean usePost) { return new HttpConfiguration(10_000, Collections.emptyMap(), null, useReport, usePost); } @@ -178,4 +195,65 @@ public void fdv1FallbackSynchronizerSendsGetByDefault() throws Exception { } } } + + @Test + public void fdv1FallbackSynchronizerConfiguredForModeUsesServiceEndpointsOverride() throws Exception { + try (HttpServer sdkPollingServer = HttpServer.start(Handlers.bodyJson(VALID_FDV1_JSON)); + HttpServer overrideServer = HttpServer.start(Handlers.bodyJson(VALID_FDV1_JSON))) { + ModeDefinition streaming = Components.dataSystem() + .customizeConnectionMode(ConnectionMode.STREAMING, + DataSystemComponents.customMode() + .synchronizers(DataSystemComponents.streamingSynchronizer()) + .fdv1FallbackSynchronizer(DataSystemComponents.fdv1FallbackSynchronizer() + .serviceEndpointsOverride(Components.serviceEndpoints() + .polling(overrideServer.getUri())))) + .buildModeTable(false) + .get(ConnectionMode.STREAMING); + + // Start the mode's FDv1 fallback synchronizer and wait for its first poll. + Synchronizer sync = streaming.getFdv1FallbackSynchronizer() + .build(makeInputs(sdkPollingServer, httpConfig(false, false))); + try { + sync.next().get(5, TimeUnit.SECONDS); + + // The poll went to the override endpoint and not to the SDK's polling endpoint. + RequestInfo req = overrideServer.getRecorder().requireRequest(); + assertTrue("path should use the FDv1 GET endpoint with a context segment", + req.getPath().startsWith("/msdk/evalx/contexts/")); + assertEquals(0, sdkPollingServer.getRecorder().count()); + } finally { + sync.close(); + } + } + } + + @Test + public void fdv1FallbackSynchronizerConfiguredForModeUsesPollIntervalAndSdkPollingEndpoint() throws Exception { + try (HttpServer server = HttpServer.start(Handlers.bodyJson(VALID_FDV1_JSON))) { + ModeDefinition streaming = Components.dataSystem() + .customizeConnectionMode(ConnectionMode.STREAMING, + DataSystemComponents.customMode() + .synchronizers(DataSystemComponents.streamingSynchronizer()) + .fdv1FallbackSynchronizer(DataSystemComponents.fdv1FallbackSynchronizer() + .pollIntervalMillis(600_000))) + .buildModeTable(false) + .get(ConnectionMode.STREAMING); + + // Start the mode's FDv1 fallback synchronizer and wait for its first poll. + Synchronizer sync = streaming.getFdv1FallbackSynchronizer() + .build(makeInputs(server, httpConfig(false, false))); + try { + sync.next().get(5, TimeUnit.SECONDS); + + // Without an endpoints override the poll goes to the SDK's polling endpoint, + // and the synchronizer polls at the configured interval. + RequestInfo req = server.getRecorder().requireRequest(); + assertTrue("path should use the FDv1 GET endpoint with a context segment", + req.getPath().startsWith("/msdk/evalx/contexts/")); + assertEquals(600_000, executor.lastFixedDelayMillis); + } finally { + sync.close(); + } + } + } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilderTest.java index de4ae6f4a..6259727da 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilderTest.java @@ -1,7 +1,10 @@ package com.launchdarkly.sdk.android.integrations; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import com.launchdarkly.sdk.android.DataSystemComponents; @@ -59,4 +62,30 @@ public void freshBuilder_hasEmptyLists() { assertEquals(0, b.getInitializerEntries().size()); assertEquals(0, b.getSynchronizerEntries().size()); } + + @Test + public void freshBuilder_neitherConfiguresNorDisablesFdv1Fallback() { + ConnectionModeBuilder b = DataSystemComponents.customMode(); + assertNull(b.getFdv1FallbackSynchronizerEntry()); + assertFalse(b.isFdv1FallbackDisabled()); + } + + @Test + public void disableFdv1Fallback_replacesConfiguredFdv1FallbackSynchronizer() { + ConnectionModeBuilder b = DataSystemComponents.customMode() + .fdv1FallbackSynchronizer(DataSystemComponents.fdv1FallbackSynchronizer()) + .disableFdv1Fallback(); + assertNull(b.getFdv1FallbackSynchronizerEntry()); + assertTrue(b.isFdv1FallbackDisabled()); + } + + @Test + public void fdv1FallbackSynchronizer_replacesEarlierDisable() { + FDv1FallbackSynchronizerEntry fallback = DataSystemComponents.fdv1FallbackSynchronizer(); + ConnectionModeBuilder b = DataSystemComponents.customMode() + .disableFdv1Fallback() + .fdv1FallbackSynchronizer(fallback); + assertSame(fallback, b.getFdv1FallbackSynchronizerEntry()); + assertFalse(b.isFdv1FallbackDisabled()); + } }