Skip to content

Commit

Permalink
Fix StackOverflowError when merging ParseObject from JSON (#925)
Browse files Browse the repository at this point in the history
* Add test for StackOverflowError during mergeFromServer

This test shows the #896 bug.

* Make a synchronized copy of availableKeys

`synchronizedSet(s)` ends up making a new wrapper for the supplied set, which
means on subsequent calls, it becomes a SynchronizedSet, wrapped in a
SynchronizedSet, wrapped ... etc.

When using the LocalDataStore, the ParseObject.State is reused for every
matching ClassName+ObjectId pair, so every time a ParseObject is parsed from
JSON data, the existing object is "refreshed" by making a copy of its State,
and then merging with the new data. Every call to State.newBuilder() is
therefore adding a new layer of SynchronizedSet to the availableKeys Set.

Eventually that nested hierarchy of sets becomes so large that it causes a
StackOverflowError for any operation on the collection.

By making a copy of the set before wrapping it in synchronizedSet(), that
nested hierarchy becomes severed, and we end up with just one level of
wrappers.

* Accept the updated Android Build-Tools 27 license
  • Loading branch information
Joe authored and Jawnnypoo committed Feb 14, 2019
1 parent 5bd91aa commit f2e6475
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jdk:
before_install:
- pip install --user codecov
- mkdir "$ANDROID_HOME/licenses" || true
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license"

script:
- ./gradlew clean testDebugUnitTest jacocoTestReport
Expand Down
2 changes: 1 addition & 1 deletion parse/src/main/java/com/parse/ParseObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -4076,7 +4076,7 @@ public Init(String className) {
objectId = state.objectId();
createdAt = state.createdAt();
updatedAt = state.updatedAt();
availableKeys = Collections.synchronizedSet(state.availableKeys());
availableKeys = Collections.synchronizedSet(new HashSet<>(state.availableKeys()));
for (String key : state.keySet()) {
serverData.put(key, state.get(key));
availableKeys.add(key);
Expand Down
27 changes: 26 additions & 1 deletion parse/src/test/java/com/parse/ParseObjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -128,7 +129,7 @@ public void testFromJSONPayload() throws JSONException {

//endregion

//region testGetter
//region testFromJson

@Test
public void testFromJSONPayloadWithoutClassname() throws JSONException {
Expand All @@ -137,6 +138,30 @@ public void testFromJSONPayloadWithoutClassname() throws JSONException {
assertNull(parseObject);
}

@Test
public void testFromJsonWithLdsStackOverflow() throws JSONException {
ParseObject localObj = ParseObject.createWithoutData("GameScore", "TT1ZskATqS");
OfflineStore lds = mock(OfflineStore.class);
Parse.setLocalDatastore(lds);

when(lds.getObject(eq("GameScore"), eq("TT1ZskATqS"))).thenReturn(localObj);

JSONObject json = new JSONObject("{" +
"\"className\":\"GameScore\"," +
"\"createdAt\":\"2015-06-22T21:23:41.733Z\"," +
"\"objectId\":\"TT1ZskATqS\"," +
"\"updatedAt\":\"2015-06-22T22:06:18.104Z\"" +
"}");
ParseObject obj;
for (int i = 0; i < 50000; i++) {
obj = ParseObject.fromJSON(json, "GameScore", ParseDecoder.get(), Collections.<String>emptySet());
}
}

//endregion

//region testGetter

@Test
public void testRevert() throws ParseException {
List<Task<Void>> tasks = new ArrayList<>();
Expand Down

0 comments on commit f2e6475

Please sign in to comment.