Skip to content

Commit

Permalink
feat: honor polling interval across process restarts
Browse files Browse the repository at this point in the history
  • Loading branch information
tanderson-ld committed Nov 19, 2024
1 parent 2da730a commit 08dc14c
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,25 @@ final class ClientContextImpl extends ClientContext {
private final FeatureFetcher fetcher;
private final PlatformState platformState;
private final TaskExecutor taskExecutor;
private final PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData;

ClientContextImpl(
ClientContext base,
DiagnosticStore diagnosticStore,
FeatureFetcher fetcher,
PlatformState platformState,
TaskExecutor taskExecutor
TaskExecutor taskExecutor,
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData
) {
super(base);
this.diagnosticStore = diagnosticStore;
this.fetcher = fetcher;
this.platformState = platformState;
this.taskExecutor = taskExecutor;
this.perEnvironmentData = perEnvironmentData;
}

// TODO: consider re-ordering so perEnvironmentData is earlier in list of params
static ClientContextImpl fromConfig(
LDConfig config,
String mobileKey,
Expand All @@ -56,7 +60,8 @@ static ClientContextImpl fromConfig(
LDLogger logger,
PlatformState platformState,
IEnvironmentReporter environmentReporter,
TaskExecutor taskExecutor
TaskExecutor taskExecutor,
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData
) {
boolean initiallyInBackground = platformState != null && !platformState.isForeground();
ClientContext minimalContext = new ClientContext(mobileKey, environmentReporter, logger, config,
Expand All @@ -82,14 +87,14 @@ static ClientContextImpl fromConfig(
if (!config.getDiagnosticOptOut()) {
diagnosticStore = new DiagnosticStore(EventUtil.makeDiagnosticParams(baseClientContext));
}
return new ClientContextImpl(baseClientContext, diagnosticStore, fetcher, platformState, taskExecutor);
return new ClientContextImpl(baseClientContext, diagnosticStore, fetcher, platformState, taskExecutor, perEnvironmentData);
}

public static ClientContextImpl get(ClientContext context) {
if (context instanceof ClientContextImpl) {
return (ClientContextImpl)context;
}
return new ClientContextImpl(context, null, null, null, null);
return new ClientContextImpl(context, null, null, null, null, null);
}

public static ClientContextImpl forDataSource(
Expand Down Expand Up @@ -119,7 +124,8 @@ public static ClientContextImpl forDataSource(
baseContextImpl.getDiagnosticStore(),
baseContextImpl.getFetcher(),
baseContextImpl.getPlatformState(),
baseContextImpl.getTaskExecutor()
baseContextImpl.getTaskExecutor(),
baseContextImpl.getPerEnvironmentData()
);
}

Expand All @@ -134,7 +140,8 @@ public ClientContextImpl setEvaluationContext(LDContext context) {
this.diagnosticStore,
this.fetcher,
this.platformState,
this.taskExecutor
this.taskExecutor,
this.perEnvironmentData
);
}

Expand All @@ -154,6 +161,10 @@ public TaskExecutor getTaskExecutor() {
return throwExceptionIfNull(taskExecutor);
}

public PersistentDataStoreWrapper.PerEnvironmentData getPerEnvironmentData() {
return throwExceptionIfNull(perEnvironmentData);
}

private static <T> T throwExceptionIfNull(T o) {
if (o == null) {
throw new IllegalStateException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,30 +244,45 @@ static final class PollingDataSourceBuilderImpl extends PollingDataSourceBuilder
implements DiagnosticDescription, DataSourceRequiresFeatureFetcher {
@Override
public DataSource build(ClientContext clientContext) {
clientContext.getDataSourceUpdateSink().setStatus(
clientContext.isInBackground() ? ConnectionInformation.ConnectionMode.BACKGROUND_POLLING :
ClientContextImpl clientContextImpl = ClientContextImpl.get(clientContext);
clientContextImpl.getDataSourceUpdateSink().setStatus(
clientContextImpl.isInBackground() ? ConnectionInformation.ConnectionMode.BACKGROUND_POLLING :
ConnectionInformation.ConnectionMode.POLLING,
null
);
int actualPollIntervalMillis = clientContext.isInBackground() ? backgroundPollIntervalMillis :

int pollInterval = clientContextImpl.isInBackground() ? backgroundPollIntervalMillis :
pollIntervalMillis;
int initialDelayMillis;

long initialDelayMillis;
if (clientContext.isInBackground() && Boolean.FALSE.equals(clientContext.getPreviouslyInBackground())) {
// If we're transitioning from foreground to background, then we don't want to do a
// poll right away because we already have recent flag data. Start polling *after*
// the first background poll interval.
initialDelayMillis = backgroundPollIntervalMillis;
} else {
// If we're in the foreground-- or, if we're in the background but we started out
// that way rather than transitioning-- then we should do the first poll right away.
initialDelayMillis = 0;
// TODO: refactor context hashing calls to use a common util
String hashed = new ContextHasher().hash(clientContextImpl.getEvaluationContext().getFullyQualifiedKey());

long lastUpdated = 0; // assume last updated is beginning of time
for (ContextIndex.IndexEntry entry : clientContextImpl.getPerEnvironmentData().getIndex().data) {
if (entry.contextId.equals(hashed)) {
lastUpdated = entry.timestamp;
}
}

// 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.
long elapsedSinceUpdate = System.currentTimeMillis() - lastUpdated;
initialDelayMillis = Math.max(pollInterval - elapsedSinceUpdate, 0);
}
ClientContextImpl clientContextImpl = ClientContextImpl.get(clientContext);

return new PollingDataSource(
clientContext.getEvaluationContext(),
clientContext.getDataSourceUpdateSink(),
clientContextImpl.getEvaluationContext(),
clientContextImpl.getDataSourceUpdateSink(),
initialDelayMillis,
actualPollIntervalMillis,
pollInterval,
clientContextImpl.getFetcher(),
clientContextImpl.getPlatformState(),
clientContextImpl.getTaskExecutor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ public boolean upsert(@NonNull LDContext context, @NonNull Flag flag) {

String contextId = hashedContextId(context);
environmentStore.setContextData(contextId, updatedFlags);

if (index == null) {
index = environmentStore.getIndex();
}
index = index.updateTimestamp(contextId, System.currentTimeMillis());
environmentStore.setIndex(index);
}

Collection<String> updatedFlag = Collections.singletonList(flag.getKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ protected LDClient(
FeatureFetcher fetcher = null;
if (config.dataSource instanceof ComponentsImpl.DataSourceRequiresFeatureFetcher) {
ClientContextImpl minimalContext = ClientContextImpl.fromConfig(config, mobileKey,
environmentName, null, initialContext, logger, platformState, environmentReporter, taskExecutor
environmentName, null, initialContext, logger, platformState, environmentReporter, taskExecutor, environmentStore
);
fetcher = new HttpFeatureFlagFetcher(minimalContext);
}
Expand All @@ -344,7 +344,8 @@ protected LDClient(
logger,
platformState,
environmentReporter,
taskExecutor
taskExecutor,
environmentStore
);

this.contextDataManager = new ContextDataManager(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
final class PollingDataSource implements DataSource {
private final LDContext currentContext;
private final DataSourceUpdateSink dataSourceUpdateSink;
final int initialDelayMillis; // visible for testing
final int pollIntervalMillis; // visible for testing
final long initialDelayMillis; // visible for testing
final long pollIntervalMillis; // visible for testing
private final FeatureFetcher fetcher;
private final PlatformState platformState;
private final TaskExecutor taskExecutor;
Expand All @@ -32,8 +32,8 @@ final class PollingDataSource implements DataSource {
PollingDataSource(
LDContext currentContext,
DataSourceUpdateSink dataSourceUpdateSink,
int initialDelayMillis,
int pollIntervalMillis,
long initialDelayMillis,
long pollIntervalMillis,
FeatureFetcher fetcher,
PlatformState platformState,
TaskExecutor taskExecutor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ private void createTestManager(
logging.logger,
mockPlatformState,
environmentReporter,
taskExecutor
taskExecutor,
environmentStore
);

contextDataManager = new ContextDataManager(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ protected ContextDataManager createDataManager(int maxCachedContexts) {
logging.logger,
null,
environmentReporter,
taskExecutor
taskExecutor,
environmentStore
);
return new ContextDataManager(
clientContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public void customDiagnosticConfigurationHttp() throws Exception {

private static LDValue makeDiagnosticJson(LDConfig config) throws Exception {
ClientContext clientContext = ClientContextImpl.fromConfig(config, "", "",
null, null, LDLogger.none(), null, new EnvironmentReporterBuilder().build(), null);
null, null, LDLogger.none(), null, new EnvironmentReporterBuilder().build(), null, null);
DiagnosticStore.SdkDiagnosticParams params = EventUtil.makeDiagnosticParams(clientContext);
DiagnosticStore diagnosticStore = new DiagnosticStore(params);
MockDiagnosticEventSender mockSender = new MockDiagnosticEventSender();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Map<String, String> headersToMap(Headers headers) {
public void headersForEnvironment() {
LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("test-key").build();
ClientContext clientContext = ClientContextImpl.fromConfig(config, "test-key", "",
null, null, null, null, new EnvironmentReporterBuilder().build(), null);
null, null, null, null, new EnvironmentReporterBuilder().build(), null, null);
Map<String, String> headers = headersToMap(
LDUtil.makeHttpProperties(clientContext).toHeadersBuilder().build()
);
Expand All @@ -96,7 +96,7 @@ public void headersForEnvironmentWithTransform() {
}))
.build();
ClientContext clientContext = ClientContextImpl.fromConfig(config, "test-key", "",
null, null, null, null, new EnvironmentReporterBuilder().build(), null);
null, null, null, null, new EnvironmentReporterBuilder().build(), null, null);

expected.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE);
expected.put("Authorization", "api_key test-key");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.launchdarkly.sdk.android.AssertHelpers.requireNoMoreValues;
import static com.launchdarkly.sdk.android.AssertHelpers.requireValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
Expand All @@ -15,6 +16,7 @@
import com.launchdarkly.sdk.android.subsystems.DataSource;

import org.easymock.EasyMockSupport;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

Expand All @@ -24,8 +26,9 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class PollingDataSourceTest extends EasyMockSupport {
public class PollingDataSourceTest {
private static final LDContext CONTEXT = LDContext.create("context-key");
private static final String MOBILE_KEY = "test-mobile-key";
private static final LDConfig EMPTY_CONFIG = new LDConfig.Builder(AutoEnvAttributes.Disabled).build();

private final MockComponents.MockDataSourceUpdateSink dataSourceUpdateSink = new MockComponents.MockDataSourceUpdateSink();
Expand All @@ -34,14 +37,20 @@ public class PollingDataSourceTest extends EasyMockSupport {

private final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build();
private final SimpleTestTaskExecutor taskExecutor = new SimpleTestTaskExecutor();
private PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData;

@Rule
public LogCaptureRule logging = new LogCaptureRule();

private ClientContext makeClientContext(boolean inBackground, Boolean previouslyInBackground) {
ClientContext baseClientContext = ClientContextImpl.fromConfig(
@Before
public void before() {
perEnvironmentData = TestUtil.makeSimplePersistentDataStoreWrapper().perEnvironmentData(MOBILE_KEY);
}

private ClientContextImpl makeClientContext(boolean inBackground, Boolean previouslyInBackground) {
ClientContextImpl baseClientContext = ClientContextImpl.fromConfig(
EMPTY_CONFIG, "", "", fetcher, CONTEXT,
logging.logger, platformState, environmentReporter, taskExecutor);
logging.logger, platformState, environmentReporter, taskExecutor, perEnvironmentData);
return ClientContextImpl.forDataSource(
baseClientContext,
dataSourceUpdateSink,
Expand Down Expand Up @@ -115,6 +124,27 @@ public void firstPollIsImmediateWhenStartingInBackground() throws Exception {
}
}

@Test
public void pollingIntervalHonoredAcrossMultipleBuildCalls() throws Exception {
ClientContextImpl clientContext = makeClientContext(true, null);
PollingDataSourceBuilder builder = Components.pollingDataSource()
.pollIntervalMillis(100000)
.backgroundPollIntervalMillis(100000);

// first build should have no delay
PollingDataSource ds1 = (PollingDataSource) builder.build(clientContext);
assertEquals(0, ds1.initialDelayMillis);

// simulate successful update of context index timestamp
String hash = new ContextHasher().hash(CONTEXT.getFullyQualifiedKey());
ContextIndex newIndex = clientContext.getPerEnvironmentData().getIndex().updateTimestamp(hash, System.currentTimeMillis());
clientContext.getPerEnvironmentData().setIndex(newIndex);

// second build should have a non-zero delay due to simulated response storing a recent timestamp
PollingDataSource ds2 = (PollingDataSource) builder.build(clientContext);
assertNotEquals(0, ds2.initialDelayMillis);
}

@Test
public void firstPollHappensAfterBackgroundPollingIntervalWhenTransitioningToBackground() throws Exception {
ClientContext clientContext = makeClientContext(true, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.launchdarkly.sdk.android.subsystems.ClientContext;
import com.launchdarkly.sdk.android.subsystems.DataSource;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

Expand All @@ -23,20 +24,24 @@ public class StreamingDataSourceTest {
// the tests here cover other aspects of how the streaming data source component behaves.

private static final LDContext CONTEXT = LDContext.create("context-key");

private static final String MOBILE_KEY = "test-mobile-key";
@Rule
public LogCaptureRule logging = new LogCaptureRule();

private final MockComponents.MockDataSourceUpdateSink dataSourceUpdateSink = new MockComponents.MockDataSourceUpdateSink();
private final MockPlatformState platformState = new MockPlatformState();

private final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build();
private final SimpleTestTaskExecutor taskExecutor = new SimpleTestTaskExecutor();
private PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData;

@Before
public void before() {
perEnvironmentData = TestUtil.makeSimplePersistentDataStoreWrapper().perEnvironmentData(MOBILE_KEY);
}

private ClientContext makeClientContext(boolean inBackground, Boolean previouslyInBackground) {
ClientContext baseClientContext = ClientContextImpl.fromConfig(
new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), "", "", null, CONTEXT,
logging.logger, platformState, environmentReporter, taskExecutor);
logging.logger, platformState, environmentReporter, taskExecutor, perEnvironmentData);
return ClientContextImpl.forDataSource(
baseClientContext,
dataSourceUpdateSink,
Expand All @@ -53,7 +58,7 @@ private ClientContext makeClientContext(boolean inBackground, Boolean previously
private ClientContext makeClientContextWithFetcher() {
ClientContext baseClientContext = ClientContextImpl.fromConfig(
new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), "", "", makeFeatureFetcher(), CONTEXT,
logging.logger, platformState, environmentReporter, taskExecutor);
logging.logger, platformState, environmentReporter, taskExecutor, perEnvironmentData);
return ClientContextImpl.forDataSource(
baseClientContext,
dataSourceUpdateSink,
Expand Down

0 comments on commit 08dc14c

Please sign in to comment.