Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: polling data source now supports one shot configuration #285

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 @@ -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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For reviewers: Updated this documentation with feedback from #284.

Copy link
Member

@kinyoklion kinyoklion Dec 20, 2024

Choose a reason for hiding this comment

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

If the one shot poll is prevented (maxNumPolls = 0), are we still initialized?

Copy link
Member

Choose a reason for hiding this comment

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

(Reading further it would appear yes, so maybe we need some statement for that?)

*/
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;
}
Copy link
Contributor Author

@tanderson-ld tanderson-ld Dec 20, 2024

Choose a reason for hiding this comment

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

For reviewers: The PR I opened previously to add "polling interval across restarts" used initialDelayMillis > 0 to short circuit initialized to true. I realized that was incorrect after examining isInitialized behavior. This bug existed only in 5.5.0 which was released two weeks ago.

When maxNumberOfPolls == 0 (such as in the rate limited one shot case), isInitialized should be true because the flags are "as up to date as they can be in the current configuration", which is how isInitialized behaves currently. For example: when offline mode is set, isInitialized returns true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: add fix documentation.


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
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For reviewers: Decrementing with unlimited polls will underflow after 20,428 years. Seemed worth the simplified implementation.


// 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;
}
}
Loading