diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java index 1a59f007..bba6a25a 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java @@ -264,16 +264,27 @@ public DataSource build(ClientContext clientContext) { } // To avoid unnecessarily frequent polling requests due to process or application lifecycle, we have added - // this initial delay logic. Calculate how much time has passed since the last update, if that is less than - // the polling interval, delay by the difference, otherwise 0 delay. + // this rate limiting logic. Calculate how much time has passed since the last update, if that is less than + // the polling interval, delay to when the next poll would have occurred, otherwise 0 delay. long elapsedSinceUpdate = System.currentTimeMillis() - lastUpdated; long initialDelayMillis = Math.max(pollInterval - elapsedSinceUpdate, 0); + int maxNumPolls = -1; // negative for unlimited number of polls + if (oneShot) { + if (initialDelayMillis > 0) { + clientContext.getBaseLogger().info("One shot polling attempt will be blocked by rate limiting."); + maxNumPolls = 0; // one shot was blocked by rate limiting logic + } else { + maxNumPolls = 1; // one shot was not blocked by rate limiting logic + } + } + return new PollingDataSource( clientContextImpl.getEvaluationContext(), clientContextImpl.getDataSourceUpdateSink(), initialDelayMillis, pollInterval, + maxNumPolls, clientContextImpl.getFetcher(), clientContextImpl.getPlatformState(), clientContextImpl.getTaskExecutor(), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java index 9acd178d..348ed868 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java @@ -17,11 +17,16 @@ */ public interface LDClientInterface extends Closeable { /** - * Checks whether the client is ready to return feature flag values. This is true if either - * the client has successfully connected to LaunchDarkly and received feature flags, or the - * client has been put into offline mode (in which case it will return only default flag values). + * Returns true if the client has successfully connected to LaunchDarkly and received feature flags after + * {@link LDClient#init(Application, LDConfig, LDContext, int)} was called. Also returns true if the SDK is + * set to offline mode. * - * @return true if the client is initialized or offline + * Otherwise this returns false until the client is able to retrieve latest feature flag data from + * LaunchDarkly services. This includes not connecting to LaunchDarkly within the start wait time provided to + * {@link LDClient#init(Application, LDConfig, LDContext, int)} even if the SDK has cached feature flags. + * + * @return true if the client is able to retrieve flag data from LaunchDarkly or offline, false if the client has been + * unable to up to this point. */ boolean isInitialized(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingDataSource.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingDataSource.java index bee3789e..feaef360 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingDataSource.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingDataSource.java @@ -22,6 +22,8 @@ final class PollingDataSource implements DataSource { private final DataSourceUpdateSink dataSourceUpdateSink; final long initialDelayMillis; // visible for testing final long pollIntervalMillis; // visible for testing + private final int maxNumberOfPolls; + int numberOfPollsRemaining; // visible for testing private final FeatureFetcher fetcher; private final PlatformState platformState; private final TaskExecutor taskExecutor; @@ -36,6 +38,7 @@ final class PollingDataSource implements DataSource { * source will report success immediately as it is now running even if data has not been * fetched. * @param pollIntervalMillis interval in millis between each polling request + * @param maxNumberOfPolls the maximum number of polling attempts, unlimited if negative is provided * @param fetcher that will be used for each fetch * @param platformState used for making decisions based on platform state * @param taskExecutor that will be used to schedule the polling tasks @@ -46,6 +49,7 @@ final class PollingDataSource implements DataSource { DataSourceUpdateSink dataSourceUpdateSink, long initialDelayMillis, long pollIntervalMillis, + int maxNumberOfPolls, FeatureFetcher fetcher, PlatformState platformState, TaskExecutor taskExecutor, @@ -55,6 +59,8 @@ final class PollingDataSource implements DataSource { this.dataSourceUpdateSink = dataSourceUpdateSink; this.initialDelayMillis = initialDelayMillis; this.pollIntervalMillis = pollIntervalMillis; + this.maxNumberOfPolls = maxNumberOfPolls; + this.numberOfPollsRemaining = maxNumberOfPolls; this.fetcher = fetcher; this.platformState = platformState; this.taskExecutor = taskExecutor; @@ -63,15 +69,16 @@ final class PollingDataSource implements DataSource { @Override public void start(final Callback resultCallback) { - - if (initialDelayMillis > 0) { - // if there is an initial delay, we will immediately report the successful start of the data source + if (maxNumberOfPolls == 0) { + // If there are no polls to be made, we will immediately report the successful start of the data source. This + // may seem strange, but one can think of this data source as behaving like a no-op in this configuration. resultCallback.onSuccess(true); + return; } Runnable pollRunnable = () -> poll(resultCallback); - logger.debug("Scheduling polling task with interval of {}ms, starting after {}ms", - pollIntervalMillis, initialDelayMillis); + logger.debug("Scheduling polling task with interval of {}ms, starting after {}ms, with max number of polls of {}", + pollIntervalMillis, initialDelayMillis, maxNumberOfPolls); ScheduledFuture task = taskExecutor.startRepeatingTask(pollRunnable, initialDelayMillis, pollIntervalMillis); currentPollTask.set(task); @@ -87,7 +94,19 @@ public void stop(Callback completionCallback) { } private void poll(Callback resultCallback) { - ConnectivityManager.fetchAndSetData(fetcher, context, dataSourceUpdateSink, - resultCallback, logger); + // poll if there is no max (negative number) or there are polls remaining + if (maxNumberOfPolls < 0 || numberOfPollsRemaining > 0) { + ConnectivityManager.fetchAndSetData(fetcher, context, dataSourceUpdateSink, + resultCallback, logger); + numberOfPollsRemaining--; // decrementing even when we have unlimited polls has no consequence + } + + // terminate if we have a max number of polls and no polls remaining + if (maxNumberOfPolls >= 0 && numberOfPollsRemaining <= 0) { + ScheduledFuture task = currentPollTask.getAndSet(null); + if (task != null) { + task.cancel(true); + } + } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java index 38e48d68..9ae96cc9 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java @@ -46,6 +46,13 @@ public abstract class PollingDataSourceBuilder implements ComponentConfigurer @@ -80,4 +87,15 @@ public PollingDataSourceBuilder pollIntervalMillis(int pollIntervalMillis) { DEFAULT_POLL_INTERVAL_MILLIS : pollIntervalMillis; return this; } + + /** + * Sets the data source to make one and only one attempt to get feature flag updates. The one shot + * poll may be blocked by rate limiting logic and will not be retried if that occurs. + * + * @return the builder + */ + public PollingDataSourceBuilder oneShot() { + this.oneShot = true; + return this; + } }