Skip to content

Commit 5318bd9

Browse files
Fix StackOverflowError by deeply nested server-timestamps. (#4715)
* Adds a new test of StackOverflowError caused by deeply nested ServerTimestamps See: #4702 * Keeps just only oldest previous value. Omit history of server-timestamps to fix StackOverflowError. See: #4702 * Fix formatting. * Add changelog. --------- Co-authored-by: Ehsan Nasiri <[email protected]>
1 parent 02b3365 commit 5318bd9

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22
* [feature] Add support for disjunctions in queries (`OR` queries).
3+
* [fixed] Fixed stack overflow caused by deeply nested server timestamps (#4702).
34

45
# 24.4.4
56
* [changed] Relaxed certain query validations performed by the SDK (#4231).

firebase-firestore/src/main/java/com/google/firebase/firestore/model/ServerTimestamps.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ public static Value valueOf(Timestamp localWriteTime, @Nullable Value previousVa
6262
.putFields(TYPE_KEY, encodedType)
6363
.putFields(LOCAL_WRITE_TIME_KEY, encodeWriteTime);
6464

65-
if (previousValue != null) {
66-
mapRepresentation.putFields(PREVIOUS_VALUE_KEY, previousValue);
65+
Value actualPreviousValue = previousValue == null ? null : getPreviousValue(previousValue);
66+
if (actualPreviousValue != null) {
67+
mapRepresentation.putFields(PREVIOUS_VALUE_KEY, actualPreviousValue);
6768
}
6869

6970
return Value.newBuilder().setMapValue(mapRepresentation).build();

firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,29 @@
3232
import static java.util.Collections.emptyList;
3333
import static java.util.Collections.singletonList;
3434

35+
import com.google.firebase.Timestamp;
3536
import com.google.firebase.firestore.FieldValue;
3637
import com.google.firebase.firestore.core.Query;
38+
import com.google.firebase.firestore.model.DocumentKey;
3739
import com.google.firebase.firestore.model.FieldIndex;
40+
import com.google.firebase.firestore.model.FieldPath;
41+
import com.google.firebase.firestore.model.ObjectValue;
42+
import com.google.firebase.firestore.model.ServerTimestamps;
43+
import com.google.firebase.firestore.model.mutation.FieldMask;
44+
import com.google.firebase.firestore.model.mutation.FieldTransform;
45+
import com.google.firebase.firestore.model.mutation.PatchMutation;
46+
import com.google.firebase.firestore.model.mutation.Precondition;
47+
import com.google.firebase.firestore.model.mutation.ServerTimestampOperation;
48+
import com.google.firestore.v1.Value;
49+
import java.util.ArrayList;
3850
import java.util.Arrays;
3951
import java.util.Collection;
4052
import java.util.Collections;
53+
import java.util.HashMap;
54+
import java.util.HashSet;
55+
import java.util.List;
56+
import java.util.Map;
57+
import java.util.concurrent.atomic.AtomicReference;
4158
import org.junit.Test;
4259
import org.junit.runner.RunWith;
4360
import org.robolectric.RobolectricTestRunner;
@@ -290,4 +307,60 @@ public void testIndexesServerTimestamps() {
290307
assertOverlayTypes(keyMap("coll/a", CountingQueryEngine.OverlayType.Set));
291308
assertQueryReturned("coll/a");
292309
}
310+
311+
@Test
312+
public void testDeeplyNestedServerTimestamps() {
313+
Timestamp timestamp = Timestamp.now();
314+
Value initialServerTimestamp = ServerTimestamps.valueOf(timestamp, null);
315+
Map<String, Value> fields =
316+
new HashMap<String, Value>() {
317+
{
318+
put("timestamp", ServerTimestamps.valueOf(timestamp, initialServerTimestamp));
319+
}
320+
};
321+
FieldPath path = FieldPath.fromSingleSegment("timestamp");
322+
FieldMask mask =
323+
FieldMask.fromSet(
324+
new HashSet<FieldPath>() {
325+
{
326+
add(path);
327+
}
328+
});
329+
FieldTransform fieldTransform =
330+
new FieldTransform(path, ServerTimestampOperation.getInstance());
331+
List<FieldTransform> fieldTransforms =
332+
new ArrayList<FieldTransform>() {
333+
{
334+
add(fieldTransform);
335+
}
336+
};
337+
AtomicReference<Throwable> error = new AtomicReference<>();
338+
Thread thread =
339+
new Thread(
340+
Thread.currentThread().getThreadGroup(),
341+
() -> {
342+
try {
343+
for (int i = 0; i < 1000; ++i) {
344+
writeMutation(
345+
new PatchMutation(
346+
DocumentKey.fromPathString("some/object/for/test"),
347+
ObjectValue.fromMap(fields),
348+
mask,
349+
Precondition.NONE,
350+
fieldTransforms));
351+
}
352+
} catch (Throwable e) {
353+
error.set(e);
354+
}
355+
},
356+
"test",
357+
1024 * 1024);
358+
try {
359+
thread.start();
360+
thread.join();
361+
} catch (InterruptedException e) {
362+
throw new AssertionError(e);
363+
}
364+
assertThat(error.get()).isNull();
365+
}
293366
}

0 commit comments

Comments
 (0)