Skip to content

Commit 77e04d8

Browse files
author
Michael Lehenbauer
committed
Add priming logic to integration tests to avoid backend cold start issues.
[Port of firebase/firebase-js-sdk#1259]
1 parent 2b616dd commit 77e04d8

File tree

2 files changed

+53
-35
lines changed

2 files changed

+53
-35
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/EventAccumulator.java

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

1515
package com.google.firebase.firestore.testutil;
1616

17+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.OPERATION_WAIT_TIMEOUT_MS;
1718
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
1819
import static com.google.firebase.firestore.util.Assert.hardAssert;
1920

@@ -47,22 +48,30 @@ public EventListener<T> listener() {
4748
};
4849
}
4950

50-
public List<T> await(int numEvents) {
51+
public List<T> await(int numEvents, long timeoutMs) {
5152
synchronized (this) {
5253
hardAssert(completion == null, "calling await while another await is running");
5354
completion = new TaskCompletionSource<>();
5455
maxEvents = maxEvents + numEvents;
5556
checkFulfilled();
5657
}
5758

58-
waitFor(completion.getTask());
59+
waitFor(completion.getTask(), timeoutMs);
5960
completion = null;
6061
return events.subList(maxEvents - numEvents, maxEvents);
6162
}
6263

64+
public List<T> await(int numEvents) {
65+
return await(numEvents, OPERATION_WAIT_TIMEOUT_MS);
66+
}
67+
6368
// Await 1 event.
69+
public T await(long timeoutMs) {
70+
return await(1, timeoutMs).get(0);
71+
}
72+
6473
public T await() {
65-
return await(1).get(0);
74+
return await(1, OPERATION_WAIT_TIMEOUT_MS).get(0);
6675
}
6776

6877
/** Waits for a snapshot with pending writes. */

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.firebase.firestore.testutil.TestUtil.map;
1818
import static com.google.firebase.firestore.util.Util.autoId;
19+
import static junit.framework.Assert.assertEquals;
1920
import static junit.framework.Assert.assertNull;
2021
import static junit.framework.Assert.fail;
2122

@@ -62,20 +63,18 @@ public class IntegrationTestUtil {
6263
/** Online status of all active Firestore clients. */
6364
private static final Map<FirebaseFirestore, Boolean> firestoreStatus = new HashMap<>();
6465

65-
private static final long SEMAPHORE_WAIT_TIMEOUT_MS = 30000;
66-
private static final long SHUTDOWN_WAIT_TIMEOUT_MS = 10000;
67-
private static final long BATCH_WAIT_TIMEOUT_MS = 120000;
68-
69-
private static final FirestoreProvider provider = new FirestoreProvider();
66+
/** Default amount of time to wait for a given operation to complete, used by waitFor() helper. */
67+
static final long OPERATION_WAIT_TIMEOUT_MS = 10000;
7068

7169
/**
72-
* TODO: There's some flakiness with hexa / emulator / whatever that causes the first write in a
73-
* run to frequently time out. So for now we always send an initial write with an extra long
74-
* timeout to improve test reliability.
70+
* Firestore databases can be subject to a ~30s "cold start" delay if they have not been used
71+
* recently, so before any tests run we "prime" the backend.
7572
*/
76-
private static final long FIRST_WRITE_TIMEOUT_MS = 60000;
73+
private static final long PRIMING_TIMEOUT_MS = 45000;
74+
75+
private static final FirestoreProvider provider = new FirestoreProvider();
7776

78-
private static boolean sentFirstWrite = false;
77+
private static boolean backendPrimed = false;
7978

8079
public static FirestoreProvider provider() {
8180
return provider;
@@ -113,15 +112,37 @@ public static FirebaseFirestore testFirestore() {
113112
*/
114113
public static FirebaseFirestore testFirestore(FirebaseFirestoreSettings settings) {
115114
FirebaseFirestore firestore = testFirestore(provider.projectId(), Level.DEBUG, settings);
116-
if (!sentFirstWrite) {
117-
sentFirstWrite = true;
118-
waitFor(
119-
firestore.document("test-collection/initial-write-doc").set(map("foo", 1)),
120-
FIRST_WRITE_TIMEOUT_MS);
115+
if (!backendPrimed) {
116+
backendPrimed = true;
117+
primeBackend();
121118
}
122119
return firestore;
123120
}
124121

122+
private static void primeBackend() {
123+
EventAccumulator<DocumentSnapshot> accumulator = new EventAccumulator<>();
124+
DocumentReference docRef = testDocument();
125+
ListenerRegistration listenerRegistration = docRef.addSnapshotListener(accumulator.listener());
126+
127+
// Wait for watch to initialize and deliver first event.
128+
accumulator.awaitRemoteEvent();
129+
130+
// Use a transaction to perform a write without triggering any local events.
131+
docRef
132+
.getFirestore()
133+
.runTransaction(
134+
transaction -> {
135+
transaction.set(docRef, map("value", "done"));
136+
return null;
137+
});
138+
139+
// Wait to see the write on the watch stream.
140+
DocumentSnapshot docSnap = accumulator.await(PRIMING_TIMEOUT_MS);
141+
assertEquals("done", docSnap.get("value"));
142+
143+
listenerRegistration.remove();
144+
}
145+
125146
/** Initializes a new Firestore instance that uses a non-existing default project. */
126147
public static FirebaseFirestore testAlternateFirestore() {
127148
return testFirestore(BAD_PROJECT_ID, Level.DEBUG, newTestSettings());
@@ -181,11 +202,7 @@ public static void tearDown() {
181202
try {
182203
for (FirebaseFirestore firestore : firestoreStatus.keySet()) {
183204
Task<Void> result = AccessHelper.shutdown(firestore);
184-
try {
185-
Tasks.await(result, SHUTDOWN_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
186-
} catch (TimeoutException | ExecutionException | InterruptedException e) {
187-
throw new RuntimeException(e);
188-
}
205+
waitFor(result);
189206
}
190207
} finally {
191208
firestoreStatus.clear();
@@ -246,7 +263,7 @@ public static void waitFor(Semaphore semaphore) {
246263
public static void waitFor(Semaphore semaphore, int count) {
247264
try {
248265
boolean acquired =
249-
semaphore.tryAcquire(count, SEMAPHORE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
266+
semaphore.tryAcquire(count, OPERATION_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
250267
if (!acquired) {
251268
throw new TimeoutException("Failed to acquire semaphore within test timeout");
252269
}
@@ -257,7 +274,7 @@ public static void waitFor(Semaphore semaphore, int count) {
257274

258275
public static void waitFor(CountDownLatch countDownLatch) {
259276
try {
260-
boolean acquired = countDownLatch.await(SEMAPHORE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
277+
boolean acquired = countDownLatch.await(OPERATION_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
261278
if (!acquired) {
262279
throw new TimeoutException("Failed to acquire countdown latch within test timeout");
263280
}
@@ -266,16 +283,8 @@ public static void waitFor(CountDownLatch countDownLatch) {
266283
}
267284
}
268285

269-
public static void waitFor(List<Task<?>> task) {
270-
try {
271-
Tasks.await(Tasks.whenAll(task), BATCH_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
272-
} catch (TimeoutException | ExecutionException | InterruptedException e) {
273-
throw new RuntimeException(e);
274-
}
275-
}
276-
277286
public static <T> T waitFor(Task<T> task) {
278-
return waitFor(task, SEMAPHORE_WAIT_TIMEOUT_MS);
287+
return waitFor(task, OPERATION_WAIT_TIMEOUT_MS);
279288
}
280289

281290
public static <T> T waitFor(Task<T> task, long timeoutMS) {
@@ -288,7 +297,7 @@ public static <T> T waitFor(Task<T> task, long timeoutMS) {
288297

289298
public static <T> Exception waitForException(Task<T> task) {
290299
try {
291-
Tasks.await(task, SEMAPHORE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
300+
Tasks.await(task, OPERATION_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
292301
throw new RuntimeException("Expected Exception but Task completed successfully.");
293302
} catch (ExecutionException e) {
294303
return (Exception) e.getCause();

0 commit comments

Comments
 (0)