Skip to content

Commit f2e6475

Browse files
JoeJawnnypoo
authored andcommitted
Fix StackOverflowError when merging ParseObject from JSON (#925)
* 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
1 parent 5bd91aa commit f2e6475

File tree

3 files changed

+28
-3
lines changed

3 files changed

+28
-3
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jdk:
1111
before_install:
1212
- pip install --user codecov
1313
- mkdir "$ANDROID_HOME/licenses" || true
14-
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license"
14+
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license"
1515

1616
script:
1717
- ./gradlew clean testDebugUnitTest jacocoTestReport

parse/src/main/java/com/parse/ParseObject.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4076,7 +4076,7 @@ public Init(String className) {
40764076
objectId = state.objectId();
40774077
createdAt = state.createdAt();
40784078
updatedAt = state.updatedAt();
4079-
availableKeys = Collections.synchronizedSet(state.availableKeys());
4079+
availableKeys = Collections.synchronizedSet(new HashSet<>(state.availableKeys()));
40804080
for (String key : state.keySet()) {
40814081
serverData.put(key, state.get(key));
40824082
availableKeys.add(key);

parse/src/test/java/com/parse/ParseObjectTest.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import static org.junit.Assert.assertTrue;
4545
import static org.mockito.Matchers.any;
4646
import static org.mockito.Matchers.anyString;
47+
import static org.mockito.Matchers.eq;
4748
import static org.mockito.Mockito.mock;
4849
import static org.mockito.Mockito.when;
4950

@@ -128,7 +129,7 @@ public void testFromJSONPayload() throws JSONException {
128129

129130
//endregion
130131

131-
//region testGetter
132+
//region testFromJson
132133

133134
@Test
134135
public void testFromJSONPayloadWithoutClassname() throws JSONException {
@@ -137,6 +138,30 @@ public void testFromJSONPayloadWithoutClassname() throws JSONException {
137138
assertNull(parseObject);
138139
}
139140

141+
@Test
142+
public void testFromJsonWithLdsStackOverflow() throws JSONException {
143+
ParseObject localObj = ParseObject.createWithoutData("GameScore", "TT1ZskATqS");
144+
OfflineStore lds = mock(OfflineStore.class);
145+
Parse.setLocalDatastore(lds);
146+
147+
when(lds.getObject(eq("GameScore"), eq("TT1ZskATqS"))).thenReturn(localObj);
148+
149+
JSONObject json = new JSONObject("{" +
150+
"\"className\":\"GameScore\"," +
151+
"\"createdAt\":\"2015-06-22T21:23:41.733Z\"," +
152+
"\"objectId\":\"TT1ZskATqS\"," +
153+
"\"updatedAt\":\"2015-06-22T22:06:18.104Z\"" +
154+
"}");
155+
ParseObject obj;
156+
for (int i = 0; i < 50000; i++) {
157+
obj = ParseObject.fromJSON(json, "GameScore", ParseDecoder.get(), Collections.<String>emptySet());
158+
}
159+
}
160+
161+
//endregion
162+
163+
//region testGetter
164+
140165
@Test
141166
public void testRevert() throws ParseException {
142167
List<Task<Void>> tasks = new ArrayList<>();

0 commit comments

Comments
 (0)