Skip to content

Commit

Permalink
feat: polling data source now supports one shot configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
tanderson-ld committed Dec 20, 2024
1 parent eef46b6 commit 5fbbd63
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -46,6 +49,7 @@ final class PollingDataSource implements DataSource {
DataSourceUpdateSink dataSourceUpdateSink,
long initialDelayMillis,
long pollIntervalMillis,
int maxNumberOfPolls,
FeatureFetcher fetcher,
PlatformState platformState,
TaskExecutor taskExecutor,
Expand All @@ -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;
Expand All @@ -63,15 +69,16 @@ final class PollingDataSource implements DataSource {

@Override
public void start(final Callback<Boolean> 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);
Expand All @@ -87,7 +94,19 @@ public void stop(Callback<Void> completionCallback) {
}

private void poll(Callback<Boolean> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public abstract class PollingDataSourceBuilder implements ComponentConfigurer<Da
*/
protected int pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS;

/**
* If true, the polling data source will make at most one poll 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.
*/
protected boolean oneShot = false;

/**
* Sets the interval between feature flag updates when the application is running in the background.
* <p>
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 5fbbd63

Please sign in to comment.