Skip to content

Realtime Fetch ID #4328

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

Merged
merged 10 commits into from
Dec 1, 2022
Merged
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 @@ -40,7 +40,7 @@

public class ConfigAutoFetch {

private static final int FETCH_RETRY = 3;
private static final int MAXIMUM_FETCH_ATTEMPTS = 3;
private static final String TEMPLATE_VERSION_KEY = "latestTemplateVersionNumber";
private static final String REALTIME_DISABLED_KEY = "featureDisabled";

Expand Down Expand Up @@ -155,7 +155,7 @@ private void handleNotifications(InputStream inputStream) throws IOException {
long oldTemplateVersion = configFetchHandler.getTemplateVersionNumber();
long targetTemplateVersion = jsonObject.getLong(TEMPLATE_VERSION_KEY);
if (targetTemplateVersion > oldTemplateVersion) {
autoFetch(FETCH_RETRY, targetTemplateVersion);
autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
}
}
} catch (JSONException ex) {
Expand Down Expand Up @@ -195,7 +195,13 @@ public void run() {

@VisibleForTesting
public synchronized void fetchLatestConfig(int remainingAttempts, long targetVersion) {
Task<ConfigFetchHandler.FetchResponse> fetchTask = configFetchHandler.fetch(0L);
int currentAttempts = remainingAttempts - 1;

// fetchAttemptNumber is calculated by subtracting current attempts from the max number of
// possible attempts.
Task<ConfigFetchHandler.FetchResponse> fetchTask =
configFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, MAXIMUM_FETCH_ATTEMPTS - currentAttempts);
fetchTask.onSuccessTask(
(fetchResponse) -> {
long newTemplateVersion = 0;
Expand All @@ -214,7 +220,7 @@ public synchronized void fetchLatestConfig(int remainingAttempts, long targetVer
"Fetched template version is the same as SDK's current version."
+ " Retrying fetch.");
// Continue fetching until template version number if greater then current.
autoFetch(remainingAttempts - 1, targetVersion);
autoFetch(currentAttempts, targetVersion);
}
return Tasks.forResult(null);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public class ConfigFetchHandler {
*/
@VisibleForTesting static final String FIRST_OPEN_TIME_KEY = "_fot";

/** Custom Http header key to identify the fetch type. */
private static final String X_FIREBASE_RC_FETCH_TYPE = "X-Firebase-RC-Fetch-Type";

private final FirebaseInstallationsApi firebaseInstallations;
private final Provider<AnalyticsConnector> analyticsConnector;

Expand Down Expand Up @@ -156,13 +159,66 @@ public Task<FetchResponse> fetch() {
* updates, the {@link FetchResponse}'s configs will be {@code null}.
*/
public Task<FetchResponse> fetch(long minimumFetchIntervalInSeconds) {

// Make a copy to prevent any concurrency issues between Fetches.
Map<String, String> copyOfCustomHttpHeaders = new HashMap<>(customHttpHeaders);
copyOfCustomHttpHeaders.put(X_FIREBASE_RC_FETCH_TYPE, FetchType.BASE.getValue() + "/" + 1);

return fetchedConfigsCache
.get()
.continueWithTask(
executor,
(cachedFetchConfigsTask) ->
fetchIfCacheExpiredAndNotThrottled(
cachedFetchConfigsTask, minimumFetchIntervalInSeconds));
cachedFetchConfigsTask,
minimumFetchIntervalInSeconds,
copyOfCustomHttpHeaders));
}

/**
* Starts fetching configs from the Firebase Remote Config server.
*
* <p>Guarantees consistency between memory and disk; fetched configs are saved to memory only
* after they have been written to disk.
*
* <p>Fetches even if the read of the fetch cache fails (assumes there are no cached fetched
* configs in that case).
*
* <p>If the fetch request could not be created or there was error connecting to the server, the
* returned Task throws a {@link FirebaseRemoteConfigClientException}.
*
* <p>If the server responds with an error, the returned Task throws a {@link
* FirebaseRemoteConfigServerException}.
*
* <p>If any of the following is true, then the returned Task throws a {@link
* FirebaseRemoteConfigFetchThrottledException}:
*
* <ul>
* <li>The backoff duration from a previous throttled exception has not expired,
* <li>The backend responded with a throttled error, or
* <li>The backend responded with unavailable errors for the last two fetch requests.
* </ul>
*
* @param {@link FetchType} and fetchAttemptNumber help detail what started the fetch call.
* @return A {@link Task} representing an immediate fetch call that returns a {@link
* FetchResponse} with the configs fetched from the backend. If the backend was not called or
* the backend had no updates, the {@link FetchResponse}'s configs will be {@code null}.
*/
public Task<FetchResponse> fetchNowWithTypeAndAttemptNumber(
FetchType fetchType, int fetchAttemptNumber) {

// Make a copy to prevent any concurrency issues between Fetches.
Map<String, String> copyOfCustomHttpHeaders = new HashMap<>(customHttpHeaders);
copyOfCustomHttpHeaders.put(
X_FIREBASE_RC_FETCH_TYPE, fetchType.getValue() + "/" + fetchAttemptNumber);

return fetchedConfigsCache
.get()
.continueWithTask(
executor,
(cachedFetchConfigsTask) ->
fetchIfCacheExpiredAndNotThrottled(
cachedFetchConfigsTask, 0, copyOfCustomHttpHeaders));
}

/**
Expand All @@ -173,7 +229,9 @@ public Task<FetchResponse> fetch(long minimumFetchIntervalInSeconds) {
* fetch time and {@link BackoffMetadata} in {@link ConfigMetadataClient}.
*/
private Task<FetchResponse> fetchIfCacheExpiredAndNotThrottled(
Task<ConfigContainer> cachedFetchConfigsTask, long minimumFetchIntervalInSeconds) {
Task<ConfigContainer> cachedFetchConfigsTask,
long minimumFetchIntervalInSeconds,
Map<String, String> customFetchHeaders) {
Date currentTime = new Date(clock.currentTimeMillis());
if (cachedFetchConfigsTask.isSuccessful()
&& areCachedFetchConfigsValid(minimumFetchIntervalInSeconds, currentTime)) {
Expand Down Expand Up @@ -218,7 +276,7 @@ && areCachedFetchConfigsValid(minimumFetchIntervalInSeconds, currentTime)) {
String installationId = installationIdTask.getResult();
String installationToken = installationAuthTokenTask.getResult().getToken();
return fetchFromBackendAndCacheResponse(
installationId, installationToken, currentTime);
installationId, installationToken, currentTime, customFetchHeaders);
});
}

Expand Down Expand Up @@ -278,9 +336,13 @@ private String createThrottledMessage(long throttledDurationInMillis) {
* {@code fetchedConfigsCache}.
*/
private Task<FetchResponse> fetchFromBackendAndCacheResponse(
String installationId, String installationToken, Date fetchTime) {
String installationId,
String installationToken,
Date fetchTime,
Map<String, String> customFetchHeaders) {
try {
FetchResponse fetchResponse = fetchFromBackend(installationId, installationToken, fetchTime);
FetchResponse fetchResponse =
fetchFromBackend(installationId, installationToken, fetchTime, customFetchHeaders);
if (fetchResponse.getStatus() != Status.BACKEND_UPDATES_FETCHED) {
return Tasks.forResult(fetchResponse);
}
Expand All @@ -303,7 +365,10 @@ private Task<FetchResponse> fetchFromBackendAndCacheResponse(
*/
@WorkerThread
private FetchResponse fetchFromBackend(
String installationId, String installationToken, Date currentTime)
String installationId,
String installationToken,
Date currentTime,
Map<String, String> customFetchHeaders)
throws FirebaseRemoteConfigException {
try {
HttpURLConnection urlConnection = frcBackendApiClient.createHttpURLConnection();
Expand All @@ -315,7 +380,7 @@ private FetchResponse fetchFromBackend(
installationToken,
getUserProperties(),
frcMetadata.getLastFetchETag(),
customHttpHeaders,
customFetchHeaders,
getFirstOpenTime(),
currentTime);

Expand Down Expand Up @@ -626,4 +691,19 @@ public ConfigContainer getFetchedConfigs() {
int LOCAL_STORAGE_USED = 2;
}
}

public enum FetchType {
BASE("Base"),
REALTIME("Realtime");

private final String value;

FetchType(String value) {
this.value = value;
}

String getValue() {
return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,9 @@ public void realtime_stream_listen_and_retry_success() throws Exception {
new ByteArrayInputStream(
"{ \"latestTemplateVersionNumber\": 1 }".getBytes(StandardCharsets.UTF_8)));
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();

verify(mockRetryListener).onEvent();
Expand Down Expand Up @@ -1245,7 +1247,9 @@ public void realtime_stream_listen_and_failsafe_disabled() throws Exception {
"{ \"featureDisabled\": false, \"latestTemplateVersionNumber\": 2 }"
.getBytes(StandardCharsets.UTF_8)));
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();

verify(mockUnavailableEventListener, never())
Expand All @@ -1258,7 +1262,9 @@ public void realtime_stream_listen_get_inputstream_fail() throws Exception {
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class);
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.listenForNotifications();

verify(mockInvalidMessageEventListener).onError(any(FirebaseRemoteConfigClientException.class));
Expand All @@ -1267,16 +1273,20 @@ public void realtime_stream_listen_get_inputstream_fail() throws Exception {
@Test
public void realtime_stream_autofetch_success() {
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.fetchLatestConfig(1, 1);
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 1))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.fetchLatestConfig(3, 1);

verify(mockOnEventListener).onEvent();
}

@Test
public void realtime_stream_autofetch_failure() {
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
ConfigFetchHandler.FetchType.REALTIME, 3))
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
configAutoFetch.fetchLatestConfig(1, 1000);

verify(mockNotFetchedEventListener).onError(any(FirebaseRemoteConfigServerException.class));
Expand Down