Skip to content

Commit

Permalink
feat: honor polling interval across process restarts (#282)
Browse files Browse the repository at this point in the history
**Requirements**

- [x] I have added test coverage for new or changed functionality

- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
- [x] I have validated my changes against all supported platform
versions

**Related issues**

SDK-881

**Describe the solution you've provided**

Polling data source builder now uses context index timestamp to
calculate if an initial polling delay.
  • Loading branch information
tanderson-ld authored Nov 22, 2024
1 parent 2da730a commit 9b3ca70
Show file tree
Hide file tree
Showing 22 changed files with 285 additions and 202 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class LDClientEndToEndTest {
private Application application;
private MockWebServer mockPollingServer;
private URI mockPollingServerUri;
private final PersistentDataStore store = new InMemoryPersistentDataStore();
private PersistentDataStore store;

@Rule
public final ActivityScenarioRule<TestActivity> testScenario =
Expand All @@ -65,6 +65,11 @@ public void setUp() {
});
}

@Before
public void before() {
store = new InMemoryPersistentDataStore();
}

@After
public void after() throws IOException {
mockPollingServer.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,29 @@ 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;
}

static ClientContextImpl fromConfig(
LDConfig config,
String mobileKey,
String environmentName,
FeatureFetcher fetcher,
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData, FeatureFetcher fetcher,
LDContext initialContext,
LDLogger logger,
PlatformState platformState,
Expand Down Expand Up @@ -82,14 +85,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 +122,8 @@ public static ClientContextImpl forDataSource(
baseContextImpl.getDiagnosticStore(),
baseContextImpl.getFetcher(),
baseContextImpl.getPlatformState(),
baseContextImpl.getTaskExecutor()
baseContextImpl.getTaskExecutor(),
baseContextImpl.getPerEnvironmentData()
);
}

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

Expand All @@ -154,6 +159,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,36 @@ 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;
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;

// get the last updated timestamp for this context
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData = clientContextImpl.getPerEnvironmentData();
String hashedContextId = LDUtil.urlSafeBase64HashedContextId(clientContextImpl.getEvaluationContext());
String fingerprint = LDUtil.urlSafeBase64Hash(clientContextImpl.getEvaluationContext());
Long lastUpdated = perEnvironmentData.getLastUpdated(hashedContextId, fingerprint);
if (lastUpdated == null) {
lastUpdated = 0L; // default to beginning of time
}
ClientContextImpl clientContextImpl = ClientContextImpl.get(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.
long elapsedSinceUpdate = System.currentTimeMillis() - lastUpdated;
long initialDelayMillis = Math.max(pollInterval - elapsedSinceUpdate, 0);

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 @@ -447,7 +447,7 @@ public void onSuccess(String flagsJson) {
@Override
public void onError(Throwable e) {
logger.error("Error when attempting to get flag data: [{}] [{}]: {}",
LDUtil.base64Url(contextToFetch),
LDUtil.urlSafeBase64(contextToFetch),
contextToFetch,
LogValues.exceptionSummary(e));
resultCallback.onError(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
* deferred listener calls are done via the {@link TaskExecutor} abstraction.
*/
final class ContextDataManager {
static final ContextHasher HASHER = new ContextHasher();

private final PersistentDataStoreWrapper.PerEnvironmentData environmentStore;
private final int maxCachedContexts;
private final TaskExecutor taskExecutor;
Expand All @@ -57,14 +55,15 @@ final class ContextDataManager {

@NonNull private volatile LDContext currentContext;
@NonNull private volatile EnvironmentData flags = new EnvironmentData();
@NonNull private volatile ContextIndex index = null;
@NonNull private volatile ContextIndex index;

ContextDataManager(
@NonNull ClientContext clientContext,
@NonNull PersistentDataStoreWrapper.PerEnvironmentData environmentStore,
int maxCachedContexts
) {
this.environmentStore = environmentStore;
this.index = environmentStore.getIndex();
this.maxCachedContexts = maxCachedContexts;
this.taskExecutor = ClientContextImpl.get(clientContext).getTaskExecutor();
this.logger = clientContext.getBaseLogger();
Expand Down Expand Up @@ -120,8 +119,9 @@ private void initDataInternal(
@NonNull EnvironmentData newData,
boolean writeFlagsToPersistentStore
) {
List<String> removedContextIds = new ArrayList<>();
String contextId = hashedContextId(context);

String contextId = LDUtil.urlSafeBase64HashedContextId(context);
String fingerprint = LDUtil.urlSafeBase64Hash(context);
EnvironmentData oldData;
ContextIndex newIndex;

Expand All @@ -133,26 +133,27 @@ private void initDataInternal(

oldData = flags;
flags = newData;
if (index == null) {
index = environmentStore.getIndex();
}
newIndex = index.updateTimestamp(contextId, System.currentTimeMillis())
.prune(maxCachedContexts, removedContextIds);
index = newIndex;

for (String removedContextId: removedContextIds) {
environmentStore.removeContextData(removedContextId);
logger.debug("Removed flag data for context {} from persistent store", removedContextId);
}
if (writeFlagsToPersistentStore && maxCachedContexts != 0) {
environmentStore.setContextData(contextId, newData);
if (writeFlagsToPersistentStore) {
List<String> removedContextIds = new ArrayList<>();
newIndex = index.updateTimestamp(contextId, System.currentTimeMillis())
.prune(maxCachedContexts, removedContextIds);
index = newIndex;

for (String removedContextId: removedContextIds) {
environmentStore.removeContextData(removedContextId);
logger.debug("Removed flag data for context {} from persistent store", removedContextId);
}

environmentStore.setContextData(contextId, fingerprint, newData);
environmentStore.setIndex(newIndex);

if (logger.isEnabled(LDLogLevel.DEBUG)) {
logger.debug("Stored context index is now: {}", newIndex.toJson());
}

logger.debug("Updated flag data for context {} in persistent store", contextId);
}
environmentStore.setIndex(newIndex);
}

if (logger.isEnabled(LDLogLevel.DEBUG)) {
logger.debug("Stored context index is now: {}", newIndex.toJson());
}

// Determine which flags were updated and notify listeners, if any
Expand Down Expand Up @@ -241,8 +242,11 @@ public boolean upsert(@NonNull LDContext context, @NonNull Flag flag) {
updatedFlags = flags.withFlagUpdatedOrAdded(flag);
flags = updatedFlags;

String contextId = hashedContextId(context);
environmentStore.setContextData(contextId, updatedFlags);
String hashedContextId = LDUtil.urlSafeBase64HashedContextId(context);
String fingerprint = LDUtil.urlSafeBase64Hash(context);
environmentStore.setContextData(hashedContextId, fingerprint, updatedFlags);
index = index.updateTimestamp(hashedContextId, System.currentTimeMillis());
environmentStore.setIndex(index);
}

Collection<String> updatedFlag = Collections.singletonList(flag.getKey());
Expand Down Expand Up @@ -292,10 +296,6 @@ public Collection<FeatureFlagChangeListener> getListenersByKey(String key) {
return res == null ? new HashSet<>() : res;
}

public static String hashedContextId(final LDContext context) {
return HASHER.hash(context.getFullyQualifiedKey());
}

/**
* Attempts to retrieve data for the specified context, if any, from the persistent store. This
* does not affect the current context/flags state.
Expand All @@ -305,7 +305,7 @@ public static String hashedContextId(final LDContext context) {
*/
@VisibleForTesting
public @Nullable EnvironmentData getStoredData(LDContext context) {
return environmentStore.getContextData(hashedContextId(context));
return environmentStore.getContextData(LDUtil.urlSafeBase64HashedContextId(context));
}

private void notifyFlagListeners(Collection<String> updatedFlagKeys) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private Request getDefaultRequest(LDContext ldContext) throws IOException {
// and methods like Uri.withAppendedPath, simply to minimize the amount of code that relies on
// Android-specific APIs so our components are more easily unit-testable.
URI uri = HttpHelpers.concatenateUriPath(pollUri, StandardEndpoints.POLLING_REQUEST_GET_BASE_PATH);
uri = HttpHelpers.concatenateUriPath(uri, LDUtil.base64Url(ldContext));
uri = HttpHelpers.concatenateUriPath(uri, LDUtil.urlSafeBase64(ldContext));
if (evaluationReasons) {
uri = URI.create(uri.toString() + "?withReasons=true");
}
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, environmentStore, null, initialContext, logger, platformState, environmentReporter, taskExecutor
);
fetcher = new HttpFeatureFlagFetcher(minimalContext);
}
Expand All @@ -339,7 +339,7 @@ protected LDClient(
config,
mobileKey,
environmentName,
fetcher,
environmentStore, fetcher,
initialContext,
logger,
platformState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,15 @@ static String urlSafeBase64Hash(String input) {
}
}

static String base64Url(final LDContext context) {
public static String urlSafeBase64HashedContextId(LDContext context) {
return urlSafeBase64Hash(context.getFullyQualifiedKey());
}

static String urlSafeBase64Hash(LDContext context) {
return urlSafeBase64Hash(JsonSerialization.serialize(context));
}

static String urlSafeBase64(LDContext context) {
return Base64.encodeToString(JsonSerialization.serialize(context).getBytes(),
Base64.URL_SAFE + Base64.NO_WRAP);
}
Expand Down
Loading

0 comments on commit 9b3ca70

Please sign in to comment.