Skip to content

Commit 01262b4

Browse files
committed
Inject ScheduledExecutorService so it can be tested.
1 parent 47c4798 commit 01262b4

File tree

6 files changed

+81
-49
lines changed

6 files changed

+81
-49
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.util.concurrent.Executor;
4545
import java.util.concurrent.ExecutorService;
4646
import java.util.concurrent.Executors;
47+
import java.util.concurrent.ScheduledExecutorService;
4748
import java.util.concurrent.atomic.AtomicReference;
4849

4950
/**
@@ -85,6 +86,7 @@ public class RemoteConfigComponent {
8586

8687
private final Context context;
8788
private final ExecutorService executorService;
89+
private final ScheduledExecutorService scheduledExecutorService;
8890
private final FirebaseApp firebaseApp;
8991
private final FirebaseInstallationsApi firebaseInstallations;
9092
private final FirebaseABTesting firebaseAbt;
@@ -105,6 +107,7 @@ public class RemoteConfigComponent {
105107
this(
106108
context,
107109
Executors.newCachedThreadPool(),
110+
Executors.newSingleThreadScheduledExecutor(),
108111
firebaseApp,
109112
firebaseInstallations,
110113
firebaseAbt,
@@ -117,13 +120,15 @@ public class RemoteConfigComponent {
117120
protected RemoteConfigComponent(
118121
Context context,
119122
ExecutorService executorService,
123+
ScheduledExecutorService scheduledExecutorService,
120124
FirebaseApp firebaseApp,
121125
FirebaseInstallationsApi firebaseInstallations,
122126
FirebaseABTesting firebaseAbt,
123127
Provider<AnalyticsConnector> analyticsConnector,
124128
boolean loadGetDefault) {
125129
this.context = context;
126130
this.executorService = executorService;
131+
this.scheduledExecutorService = scheduledExecutorService;
127132
this.firebaseApp = firebaseApp;
128133
this.firebaseInstallations = firebaseInstallations;
129134
this.firebaseAbt = firebaseAbt;
@@ -280,7 +285,8 @@ synchronized ConfigRealtimeHandler getRealtime(
280285
activatedCacheClient,
281286
context,
282287
namespace,
283-
executorService);
288+
scheduledExecutorService
289+
);
284290
}
285291

286292
private ConfigGetParameterHandler getGetHandler(

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import java.util.HashSet;
3434
import java.util.Random;
3535
import java.util.Set;
36-
import java.util.concurrent.Executors;
36+
import java.util.concurrent.ExecutorService;
3737
import java.util.concurrent.ScheduledExecutorService;
3838
import java.util.concurrent.TimeUnit;
3939
import org.json.JSONException;
@@ -53,21 +53,22 @@ public class ConfigAutoFetch {
5353
private final ConfigFetchHandler configFetchHandler;
5454
private final ConfigCacheClient activatedCache;
5555
private final ConfigUpdateListener retryCallback;
56-
private final ScheduledExecutorService scheduledExecutorService;
56+
private final ScheduledExecutorService executorService;
5757
private final Random random;
5858

5959
public ConfigAutoFetch(
6060
HttpURLConnection httpURLConnection,
6161
ConfigFetchHandler configFetchHandler,
6262
ConfigCacheClient activatedCache,
6363
Set<ConfigUpdateListener> eventListeners,
64-
ConfigUpdateListener retryCallback) {
64+
ConfigUpdateListener retryCallback,
65+
ScheduledExecutorService executorService) {
6566
this.httpURLConnection = httpURLConnection;
6667
this.configFetchHandler = configFetchHandler;
6768
this.activatedCache = activatedCache;
6869
this.eventListeners = eventListeners;
6970
this.retryCallback = retryCallback;
70-
this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
71+
this.executorService = executorService;
7172
this.random = new Random();
7273
}
7374

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

118119
// TODO: Factor ConfigUpdateListener out of internal retry logic.
119120
retryCallback.onUpdate(new HashSet<>());
120-
scheduledExecutorService.shutdownNow();
121+
executorService.shutdownNow();
121122
try {
122-
scheduledExecutorService.awaitTermination(3L, TimeUnit.SECONDS);
123+
executorService.awaitTermination(3L, TimeUnit.SECONDS);
123124
} catch (InterruptedException ex) {
124125
Log.d(TAG, "Thread Interrupted.");
125126
}
@@ -188,7 +189,7 @@ private void autoFetch(int remainingAttempts, long targetVersion) {
188189

189190
// Needs fetch to occur between 0 - 4 seconds. Randomize to not cause ddos alerts in backend
190191
int timeTillFetch = random.nextInt(4);
191-
scheduledExecutorService.schedule(
192+
executorService.schedule(
192193
new Runnable() {
193194
@Override
194195
public void run() {
@@ -206,7 +207,7 @@ public synchronized Task<Void> fetchLatestConfig(int remainingAttempts, long tar
206207

207208
return Tasks.whenAllComplete(fetchTask, activatedConfigsTask)
208209
.continueWithTask(
209-
scheduledExecutorService,
210+
executorService,
210211
(listOfUnusedCompletedTasks) -> {
211212
if (!fetchTask.isSuccessful()) {
212213
return Tasks.forException(

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException;
2525
import java.util.LinkedHashSet;
2626
import java.util.Set;
27-
import java.util.concurrent.ExecutorService;
2827
import java.util.concurrent.Future;
2928
import java.util.concurrent.FutureTask;
29+
import java.util.concurrent.ScheduledExecutorService;
3030

3131
public class ConfigRealtimeHandler {
3232

@@ -42,7 +42,7 @@ public class ConfigRealtimeHandler {
4242
private final ConfigCacheClient activatedCacheClient;
4343
private final Context context;
4444
private final String namespace;
45-
private final ExecutorService executorService;
45+
private final ScheduledExecutorService scheduledExecutorService;
4646

4747
public ConfigRealtimeHandler(
4848
FirebaseApp firebaseApp,
@@ -51,7 +51,7 @@ public ConfigRealtimeHandler(
5151
ConfigCacheClient activatedCacheClient,
5252
Context context,
5353
String namespace,
54-
ExecutorService executorService) {
54+
ScheduledExecutorService scheduledExecutorService) {
5555

5656
this.listeners = new LinkedHashSet<>();
5757
this.realtimeHttpClientTask = null;
@@ -62,7 +62,7 @@ public ConfigRealtimeHandler(
6262
this.activatedCacheClient = activatedCacheClient;
6363
this.context = context;
6464
this.namespace = namespace;
65-
this.executorService = executorService;
65+
this.scheduledExecutorService = scheduledExecutorService;
6666
}
6767

6868
private synchronized boolean canCreateRealtimeHttpClientTask() {
@@ -96,9 +96,10 @@ private synchronized void beginRealtime() {
9696
activatedCacheClient,
9797
context,
9898
namespace,
99-
listeners);
99+
listeners,
100+
scheduledExecutorService);
100101
this.realtimeHttpClientTask =
101-
this.executorService.submit(
102+
this.scheduledExecutorService.submit(
102103
new RealtimeHttpClientFutureTask(
103104
createRealtimeHttpClientTask(realtimeHttpClient), realtimeHttpClient));
104105
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import java.util.Map;
4747
import java.util.Random;
4848
import java.util.Set;
49-
import java.util.concurrent.Executors;
5049
import java.util.concurrent.ScheduledExecutorService;
5150
import java.util.concurrent.TimeUnit;
5251
import java.util.regex.Matcher;
@@ -95,11 +94,12 @@ public ConfigRealtimeHttpClient(
9594
ConfigCacheClient activatedCache,
9695
Context context,
9796
String namespace,
98-
Set<ConfigUpdateListener> listeners) {
97+
Set<ConfigUpdateListener> listeners,
98+
ScheduledExecutorService scheduledExecutorService) {
9999

100100
this.listeners = listeners;
101101
this.httpURLConnection = null;
102-
this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
102+
this.scheduledExecutorService = scheduledExecutorService;
103103

104104
// Retry parameters
105105
this.random = new Random();
@@ -307,7 +307,7 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
307307
};
308308

309309
return new ConfigAutoFetch(
310-
httpURLConnection, configFetchHandler, activatedCache, listeners, retryCallback);
310+
httpURLConnection, configFetchHandler, activatedCache, listeners, retryCallback, scheduledExecutorService);
311311
}
312312

313313
// HTTP status code that the Realtime client should retry on.

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

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.remoteconfig;
1616

17+
import static android.os.AsyncTask.THREAD_POOL_EXECUTOR;
1718
import static androidx.test.ext.truth.os.BundleSubject.assertThat;
1819
import static com.google.common.truth.Truth.assertThat;
1920
import static com.google.common.truth.Truth.assertWithMessage;
@@ -26,6 +27,7 @@
2627
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.toExperimentInfoMaps;
2728
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_ARM_VALUE_PARAM;
2829
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_PERSONALIZATION_ID_PARAM;
30+
import static com.google.firebase.remoteconfig.testutil.Assert.assertTrue;
2931
import static org.mockito.ArgumentMatchers.anyString;
3032
import static org.mockito.ArgumentMatchers.eq;
3133
import static org.mockito.Matchers.any;
@@ -42,6 +44,7 @@
4244

4345
import android.content.Context;
4446
import android.content.res.Resources;
47+
import android.os.AsyncTask;
4548
import android.os.Bundle;
4649
import androidx.annotation.NonNull;
4750
import androidx.test.core.app.ApplicationProvider;
@@ -83,7 +86,14 @@
8386
import java.util.List;
8487
import java.util.Map;
8588
import java.util.Set;
89+
import java.util.concurrent.Callable;
90+
import java.util.concurrent.CountDownLatch;
8691
import java.util.concurrent.Executor;
92+
import java.util.concurrent.ExecutorService;
93+
import java.util.concurrent.Executors;
94+
import java.util.concurrent.ScheduledExecutorService;
95+
import java.util.concurrent.TimeUnit;
96+
8797
import org.json.JSONArray;
8898
import org.json.JSONException;
8999
import org.json.JSONObject;
@@ -93,7 +103,9 @@
93103
import org.mockito.ArgumentCaptor;
94104
import org.mockito.Mock;
95105
import org.mockito.MockitoAnnotations;
106+
import org.robolectric.Robolectric;
96107
import org.robolectric.RobolectricTestRunner;
108+
import org.robolectric.android.util.concurrent.InlineExecutorService;
97109
import org.robolectric.annotation.Config;
98110
import org.robolectric.annotation.LooperMode;
99111
import org.skyscreamer.jsonassert.JSONAssert;
@@ -179,9 +191,10 @@ public final class FirebaseRemoteConfigTest {
179191
private ConfigContainer realtimeFetchedContainer;
180192
private ConfigAutoFetch configAutoFetch;
181193
private ConfigRealtimeHttpClient configRealtimeHttpClient;
182-
183194
private FetchResponse firstFetchedContainerResponse;
184195

196+
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
197+
185198
@Before
186199
public void setUp() throws Exception {
187200
DEFAULTS_MAP.put("first_default_key", "first_default_value");
@@ -324,7 +337,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
324337
mockFetchHandler,
325338
mockActivatedCache,
326339
listeners,
327-
mockRetryListener);
340+
mockRetryListener,
341+
executorService);
328342
configRealtimeHttpClient =
329343
new ConfigRealtimeHttpClient(
330344
firebaseApp,
@@ -333,7 +347,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) {
333347
mockActivatedCache,
334348
context,
335349
"firebase",
336-
listeners);
350+
listeners,
351+
executorService);
337352
}
338353

339354
@Test
@@ -1300,51 +1315,42 @@ public void realtime_stream_listen_get_inputstream_fail() throws Exception {
13001315

13011316
@Test
13021317
public void realtime_stream_autofetch_success() throws Exception {
1318+
// Setup activated configs with keys "string_param", "long_param"
1319+
loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
13031320
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
13041321
when(mockFetchHandler.fetch(0L)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
13051322

1306-
// Setup activated configs with keys "string_param", "long_param"
1307-
loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
1308-
Set<String> updatedParams = Sets.newHashSet("realtime_param");
1323+
configAutoFetch.fetchLatestConfig(1, 1);
1324+
flushTasks();
13091325

1310-
configAutoFetch
1311-
.fetchLatestConfig(1, 1)
1312-
.addOnCompleteListener(
1313-
unused -> {
1314-
verify(mockOnUpdateListener).onUpdate(updatedParams);
1315-
});
1326+
Set<String> updatedParams = Sets.newHashSet("realtime_param");
1327+
verify(mockOnUpdateListener).onUpdate(updatedParams);
13161328
}
13171329

13181330
@Test
1319-
public void realtime_autofetchBeforeActivate_callsOnUpdateWithAllFetchedParams() {
1331+
public void realtime_autofetchBeforeActivate_callsOnUpdateWithAllFetchedParams() throws Exception {
1332+
// The first call to get() returns null while the cache is loading.
1333+
loadCacheWithConfig(mockActivatedCache, null);
13201334
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
13211335
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
13221336

1323-
// The first call to get() returns null while the cache is loading.
1324-
loadCacheWithConfig(mockActivatedCache, null);
1337+
configAutoFetch.fetchLatestConfig(1, 1);
1338+
flushTasks();
13251339

13261340
Set<String> updatedParams = Sets.newHashSet("string_param", "long_param", "realtime_param");
1327-
1328-
configAutoFetch
1329-
.fetchLatestConfig(1, 1)
1330-
.addOnCompleteListener(
1331-
unused -> {
1332-
verify(mockOnUpdateListener).onUpdate(updatedParams);
1333-
});
1341+
verify(mockOnUpdateListener).onUpdate(updatedParams);
13341342
}
13351343

13361344
@Test
1337-
public void realtime_stream_autofetch_failure() {
1345+
public void realtime_stream_autofetch_failure() throws Exception {
1346+
loadCacheWithConfig(mockActivatedCache, firstFetchedContainer);
13381347
when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L);
13391348
when(mockFetchHandler.fetch(0)).thenReturn(Tasks.forResult(realtimeFetchedContainerResponse));
13401349

1341-
configAutoFetch
1342-
.fetchLatestConfig(1, 1000)
1343-
.addOnCompleteListener(
1344-
unused -> {
1345-
verify(mockNotFetchedEventListener)
1346-
.onError(any(FirebaseRemoteConfigServerException.class));
1347-
});
1350+
configAutoFetch.fetchLatestConfig(1, 1000);
1351+
flushTasks();
1352+
1353+
verify(mockNotFetchedEventListener).onError(any(FirebaseRemoteConfigServerException.class));
13481354
}
13491355

13501356
private static void loadCacheWithConfig(
@@ -1424,6 +1430,17 @@ private static FirebaseApp initializeFirebaseApp(Context context) {
14241430
.build());
14251431
}
14261432

1433+
/**
1434+
* Flush tasks on the {@code executorService}'s thread.
1435+
*
1436+
* @throws InterruptedException
1437+
*/
1438+
private void flushTasks() throws InterruptedException {
1439+
CountDownLatch latch = new CountDownLatch(1);
1440+
executorService.execute(latch::countDown);
1441+
assertTrue("Task didn't finish", latch.await(10, TimeUnit.MILLISECONDS));
1442+
}
1443+
14271444
private ConfigUpdateListener generateEmptyRealtimeListener() {
14281445
return new ConfigUpdateListener() {
14291446
@Override

0 commit comments

Comments
 (0)