Skip to content

Commit ea97203

Browse files
committed
Merge upstream branch.
2 parents d4944f5 + 993673e commit ea97203

File tree

4 files changed

+123
-25
lines changed

4 files changed

+123
-25
lines changed

firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public class RemoteConfigComponent {
8585
new HashMap<>();
8686

8787
private final Context context;
88+
// TODO: Consolidate executors.
8889
private final ExecutorService executorService;
8990
private final ScheduledExecutorService scheduledExecutorService;
9091
private final FirebaseApp firebaseApp;

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
public class ConfigAutoFetch {
4343

44-
private static final int FETCH_RETRY = 3;
44+
private static final int MAXIMUM_FETCH_ATTEMPTS = 3;
4545
private static final String TEMPLATE_VERSION_KEY = "latestTemplateVersionNumber";
4646
private static final String REALTIME_DISABLED_KEY = "featureDisabled";
4747

@@ -53,7 +53,7 @@ public class ConfigAutoFetch {
5353
private final ConfigFetchHandler configFetchHandler;
5454
private final ConfigCacheClient activatedCache;
5555
private final ConfigUpdateListener retryCallback;
56-
private final ScheduledExecutorService executorService;
56+
private final ScheduledExecutorService scheduledExecutorService;
5757
private final Random random;
5858

5959
public ConfigAutoFetch(
@@ -62,13 +62,13 @@ public ConfigAutoFetch(
6262
ConfigCacheClient activatedCache,
6363
Set<ConfigUpdateListener> eventListeners,
6464
ConfigUpdateListener retryCallback,
65-
ScheduledExecutorService executorService) {
65+
ScheduledExecutorService scheduledExecutorService) {
6666
this.httpURLConnection = httpURLConnection;
6767
this.configFetchHandler = configFetchHandler;
6868
this.activatedCache = activatedCache;
6969
this.eventListeners = eventListeners;
7070
this.retryCallback = retryCallback;
71-
this.executorService = executorService;
71+
this.scheduledExecutorService = scheduledExecutorService;
7272
this.random = new Random();
7373
}
7474

@@ -118,9 +118,9 @@ public void listenForNotifications() {
118118

119119
// TODO: Factor ConfigUpdateListener out of internal retry logic.
120120
retryCallback.onUpdate(ConfigUpdate.create(new HashSet<>()));
121-
executorService.shutdownNow();
121+
scheduledExecutorService.shutdownNow();
122122
try {
123-
executorService.awaitTermination(3L, TimeUnit.SECONDS);
123+
scheduledExecutorService.awaitTermination(3L, TimeUnit.SECONDS);
124124
} catch (InterruptedException ex) {
125125
Log.d(TAG, "Thread Interrupted.");
126126
}
@@ -162,7 +162,7 @@ private void handleNotifications(InputStream inputStream) throws IOException {
162162
long oldTemplateVersion = configFetchHandler.getTemplateVersionNumber();
163163
long targetTemplateVersion = jsonObject.getLong(TEMPLATE_VERSION_KEY);
164164
if (targetTemplateVersion > oldTemplateVersion) {
165-
autoFetch(FETCH_RETRY, targetTemplateVersion);
165+
autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
166166
}
167167
}
168168
} catch (JSONException ex) {
@@ -189,7 +189,7 @@ private void autoFetch(int remainingAttempts, long targetVersion) {
189189

190190
// Needs fetch to occur between 0 - 4 seconds. Randomize to not cause ddos alerts in backend
191191
int timeTillFetch = random.nextInt(4);
192-
executorService.schedule(
192+
scheduledExecutorService.schedule(
193193
new Runnable() {
194194
@Override
195195
public void run() {
@@ -202,12 +202,17 @@ public void run() {
202202

203203
@VisibleForTesting
204204
public synchronized Task<Void> fetchLatestConfig(int remainingAttempts, long targetVersion) {
205-
Task<ConfigFetchHandler.FetchResponse> fetchTask = configFetchHandler.fetch(0L);
205+
int remainingAttemptsAfterFetch = remainingAttempts - 1;
206+
int currentAttemptNumber = MAXIMUM_FETCH_ATTEMPTS - remainingAttemptsAfterFetch;
207+
208+
Task<ConfigFetchHandler.FetchResponse> fetchTask =
209+
configFetchHandler.fetchNowWithTypeAndAttemptNumber(
210+
ConfigFetchHandler.FetchType.REALTIME, currentAttemptNumber);
206211
Task<ConfigContainer> activatedConfigsTask = activatedCache.get();
207212

208213
return Tasks.whenAllComplete(fetchTask, activatedConfigsTask)
209214
.continueWithTask(
210-
executorService,
215+
scheduledExecutorService,
211216
(listOfUnusedCompletedTasks) -> {
212217
if (!fetchTask.isSuccessful()) {
213218
return Tasks.forException(
@@ -231,7 +236,7 @@ public synchronized Task<Void> fetchLatestConfig(int remainingAttempts, long tar
231236
"Fetched template version is the same as SDK's current version."
232237
+ " Retrying fetch.");
233238
// Continue fetching until template version number is greater then current.
234-
autoFetch(remainingAttempts - 1, targetVersion);
239+
autoFetch(remainingAttemptsAfterFetch, targetVersion);
235240
return Tasks.forResult(null);
236241
}
237242

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public class ConfigFetchHandler {
8585
*/
8686
@VisibleForTesting static final String FIRST_OPEN_TIME_KEY = "_fot";
8787

88+
/** Custom Http header key to identify the fetch type. */
89+
private static final String X_FIREBASE_RC_FETCH_TYPE = "X-Firebase-RC-Fetch-Type";
90+
8891
private final FirebaseInstallationsApi firebaseInstallations;
8992
private final Provider<AnalyticsConnector> analyticsConnector;
9093

@@ -156,13 +159,66 @@ public Task<FetchResponse> fetch() {
156159
* updates, the {@link FetchResponse}'s configs will be {@code null}.
157160
*/
158161
public Task<FetchResponse> fetch(long minimumFetchIntervalInSeconds) {
162+
163+
// Make a copy to prevent any concurrency issues between Fetches.
164+
Map<String, String> copyOfCustomHttpHeaders = new HashMap<>(customHttpHeaders);
165+
copyOfCustomHttpHeaders.put(X_FIREBASE_RC_FETCH_TYPE, FetchType.BASE.getValue() + "/" + 1);
166+
159167
return fetchedConfigsCache
160168
.get()
161169
.continueWithTask(
162170
executor,
163171
(cachedFetchConfigsTask) ->
164172
fetchIfCacheExpiredAndNotThrottled(
165-
cachedFetchConfigsTask, minimumFetchIntervalInSeconds));
173+
cachedFetchConfigsTask,
174+
minimumFetchIntervalInSeconds,
175+
copyOfCustomHttpHeaders));
176+
}
177+
178+
/**
179+
* Starts fetching configs from the Firebase Remote Config server.
180+
*
181+
* <p>Guarantees consistency between memory and disk; fetched configs are saved to memory only
182+
* after they have been written to disk.
183+
*
184+
* <p>Fetches even if the read of the fetch cache fails (assumes there are no cached fetched
185+
* configs in that case).
186+
*
187+
* <p>If the fetch request could not be created or there was error connecting to the server, the
188+
* returned Task throws a {@link FirebaseRemoteConfigClientException}.
189+
*
190+
* <p>If the server responds with an error, the returned Task throws a {@link
191+
* FirebaseRemoteConfigServerException}.
192+
*
193+
* <p>If any of the following is true, then the returned Task throws a {@link
194+
* FirebaseRemoteConfigFetchThrottledException}:
195+
*
196+
* <ul>
197+
* <li>The backoff duration from a previous throttled exception has not expired,
198+
* <li>The backend responded with a throttled error, or
199+
* <li>The backend responded with unavailable errors for the last two fetch requests.
200+
* </ul>
201+
*
202+
* @param {@link FetchType} and fetchAttemptNumber help detail what started the fetch call.
203+
* @return A {@link Task} representing an immediate fetch call that returns a {@link
204+
* FetchResponse} with the configs fetched from the backend. If the backend was not called or
205+
* the backend had no updates, the {@link FetchResponse}'s configs will be {@code null}.
206+
*/
207+
public Task<FetchResponse> fetchNowWithTypeAndAttemptNumber(
208+
FetchType fetchType, int fetchAttemptNumber) {
209+
210+
// Make a copy to prevent any concurrency issues between Fetches.
211+
Map<String, String> copyOfCustomHttpHeaders = new HashMap<>(customHttpHeaders);
212+
copyOfCustomHttpHeaders.put(
213+
X_FIREBASE_RC_FETCH_TYPE, fetchType.getValue() + "/" + fetchAttemptNumber);
214+
215+
return fetchedConfigsCache
216+
.get()
217+
.continueWithTask(
218+
executor,
219+
(cachedFetchConfigsTask) ->
220+
fetchIfCacheExpiredAndNotThrottled(
221+
cachedFetchConfigsTask, 0, copyOfCustomHttpHeaders));
166222
}
167223

168224
/**
@@ -173,7 +229,9 @@ public Task<FetchResponse> fetch(long minimumFetchIntervalInSeconds) {
173229
* fetch time and {@link BackoffMetadata} in {@link ConfigMetadataClient}.
174230
*/
175231
private Task<FetchResponse> fetchIfCacheExpiredAndNotThrottled(
176-
Task<ConfigContainer> cachedFetchConfigsTask, long minimumFetchIntervalInSeconds) {
232+
Task<ConfigContainer> cachedFetchConfigsTask,
233+
long minimumFetchIntervalInSeconds,
234+
Map<String, String> customFetchHeaders) {
177235
Date currentTime = new Date(clock.currentTimeMillis());
178236
if (cachedFetchConfigsTask.isSuccessful()
179237
&& areCachedFetchConfigsValid(minimumFetchIntervalInSeconds, currentTime)) {
@@ -218,7 +276,7 @@ && areCachedFetchConfigsValid(minimumFetchIntervalInSeconds, currentTime)) {
218276
String installationId = installationIdTask.getResult();
219277
String installationToken = installationAuthTokenTask.getResult().getToken();
220278
return fetchFromBackendAndCacheResponse(
221-
installationId, installationToken, currentTime);
279+
installationId, installationToken, currentTime, customFetchHeaders);
222280
});
223281
}
224282

@@ -278,9 +336,13 @@ private String createThrottledMessage(long throttledDurationInMillis) {
278336
* {@code fetchedConfigsCache}.
279337
*/
280338
private Task<FetchResponse> fetchFromBackendAndCacheResponse(
281-
String installationId, String installationToken, Date fetchTime) {
339+
String installationId,
340+
String installationToken,
341+
Date fetchTime,
342+
Map<String, String> customFetchHeaders) {
282343
try {
283-
FetchResponse fetchResponse = fetchFromBackend(installationId, installationToken, fetchTime);
344+
FetchResponse fetchResponse =
345+
fetchFromBackend(installationId, installationToken, fetchTime, customFetchHeaders);
284346
if (fetchResponse.getStatus() != Status.BACKEND_UPDATES_FETCHED) {
285347
return Tasks.forResult(fetchResponse);
286348
}
@@ -303,7 +365,10 @@ private Task<FetchResponse> fetchFromBackendAndCacheResponse(
303365
*/
304366
@WorkerThread
305367
private FetchResponse fetchFromBackend(
306-
String installationId, String installationToken, Date currentTime)
368+
String installationId,
369+
String installationToken,
370+
Date currentTime,
371+
Map<String, String> customFetchHeaders)
307372
throws FirebaseRemoteConfigException {
308373
try {
309374
HttpURLConnection urlConnection = frcBackendApiClient.createHttpURLConnection();
@@ -315,7 +380,7 @@ private FetchResponse fetchFromBackend(
315380
installationToken,
316381
getUserProperties(),
317382
frcMetadata.getLastFetchETag(),
318-
customHttpHeaders,
383+
customFetchHeaders,
319384
getFirstOpenTime(),
320385
currentTime);
321386

@@ -626,4 +691,19 @@ public ConfigContainer getFetchedConfigs() {
626691
int LOCAL_STORAGE_USED = 2;
627692
}
628693
}
694+
695+
public enum FetchType {
696+
BASE("Base"),
697+
REALTIME("Realtime");
698+
699+
private final String value;
700+
701+
FetchType(String value) {
702+
this.value = value;
703+
}
704+
705+
String getValue() {
706+
return value;
707+
}
708+
}
629709
}

firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,7 +1187,9 @@ public void realtime_stream_listen_and_retry_success() throws Exception {
11871187
new ByteArrayInputStream(
11881188
"{ \"latestTemplateVersionNumber\": 1 }".getBytes(StandardCharsets.UTF_8)));
11891189
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1190-
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1190+
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
1191+
ConfigFetchHandler.FetchType.REALTIME, 1))
1192+
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
11911193
configAutoFetch.listenForNotifications();
11921194

11931195
verify(mockRetryListener).onUpdate(any());
@@ -1289,7 +1291,9 @@ public void realtime_stream_listen_and_failsafe_disabled() throws Exception {
12891291
"{ \"featureDisabled\": false, \"latestTemplateVersionNumber\": 2 }"
12901292
.getBytes(StandardCharsets.UTF_8)));
12911293
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1292-
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1294+
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
1295+
ConfigFetchHandler.FetchType.REALTIME, 1))
1296+
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
12931297
configAutoFetch.listenForNotifications();
12941298

12951299
verify(mockUnavailableEventListener, never())
@@ -1302,7 +1306,9 @@ public void realtime_stream_listen_get_inputstream_fail() throws Exception {
13021306
when(mockHttpURLConnection.getResponseCode()).thenReturn(200);
13031307
when(mockHttpURLConnection.getInputStream()).thenThrow(IOException.class);
13041308
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1305-
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1309+
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
1310+
ConfigFetchHandler.FetchType.REALTIME, 1))
1311+
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
13061312
configAutoFetch.listenForNotifications();
13071313

13081314
verify(mockInvalidMessageEventListener).onError(any(FirebaseRemoteConfigClientException.class));
@@ -1313,9 +1319,11 @@ public void realtime_stream_autofetch_success() throws Exception {
13131319
// Setup activated configs with keys "string_param", "long_param"
13141320
loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
13151321
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1316-
when(mockFetchHandler.fetch(0L)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1322+
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
1323+
ConfigFetchHandler.FetchType.REALTIME, 1))
1324+
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1325+
configAutoFetch.fetchLatestConfig(3, 1);
13171326

1318-
configAutoFetch.fetchLatestConfig(1, 1);
13191327
flushScheduledTasks();
13201328

13211329
Set<String> updatedParams = Sets.newHashSet("realtime_param");
@@ -1329,7 +1337,9 @@ public void realtime_autofetchBeforeActivate_callsOnUpdateWithAllFetchedParams()
13291337
// The first call to get() returns null while the cache is loading.
13301338
loadCacheWithConfig(mockActivatedCache, null);
13311339
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1332-
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1340+
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
1341+
ConfigFetchHandler.FetchType.REALTIME, 3))
1342+
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
13331343

13341344
configAutoFetch.fetchLatestConfig(1, 1);
13351345
flushScheduledTasks();
@@ -1343,7 +1353,9 @@ public void realtime_autofetchBeforeActivate_callsOnUpdateWithAllFetchedParams()
13431353
public void realtime_stream_autofetch_failure() throws Exception {
13441354
loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
13451355
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
1346-
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
1356+
when(mockFetchHandler.fetchNowWithTypeAndAttemptNumber(
1357+
ConfigFetchHandler.FetchType.REALTIME, 3))
1358+
.thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
13471359

13481360
configAutoFetch.fetchLatestConfig(1, 1000);
13491361
flushScheduledTasks();

0 commit comments

Comments
 (0)