Skip to content

Commit d08f694

Browse files
authored
Merge pull request #182 from Leanplum/oom3
Back off 50%
2 parents 4767b81 + 861eef5 commit d08f694

File tree

3 files changed

+126
-31
lines changed

3 files changed

+126
-31
lines changed

AndroidSDKCore/src/main/java/com/leanplum/internal/Request.java

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ private void triggerErrorCallback(Exception e) {
337337
error.error(e);
338338
}
339339
if (apiResponse != null) {
340-
List<Map<String, Object>> requests = getUnsentRequests();
340+
List<Map<String, Object>> requests = getUnsentRequests(1.0);
341341
List<Map<String, Object>> requestsToSend = removeIrrelevantBackgroundStartRequests(requests);
342342
apiResponse.response(requestsToSend, null, requests.size());
343343
}
@@ -481,6 +481,12 @@ protected Void doInBackground(Void... params) {
481481
}
482482

483483

484+
/**
485+
* This class wraps the unsent requests, requests that we need to send
486+
* and the JSON encoded string. Wrapping it in the class allows us to
487+
* retain consistency in the requests we are sending and the actual
488+
* JSON string.
489+
*/
484490
static class RequestsWithEncoding {
485491
List<Map<String, Object>> unsentRequests;
486492
List<Map<String, Object>> requestsToSend;
@@ -509,21 +515,32 @@ private RequestsWithEncoding getRequestsWithEncodedStringForErrors() {
509515
return requestsWithEncoding;
510516
}
511517

512-
protected RequestsWithEncoding getRequestsWithEncodedStringStoredRequests() {
513-
List<Map<String, Object>> unsentRequests;
514-
List<Map<String, Object>> requestsToSend;
515-
String jsonEncodedRequestsToSend;
516518

517-
RequestsWithEncoding requestsWithEncoding = new RequestsWithEncoding();
518-
unsentRequests = getUnsentRequests();
519-
requestsToSend = removeIrrelevantBackgroundStartRequests(unsentRequests);
520-
jsonEncodedRequestsToSend = jsonEncodeUnsentRequests(unsentRequests);
519+
protected RequestsWithEncoding getRequestsWithEncodedStringStoredRequests(double fraction) {
520+
try {
521+
List<Map<String, Object>> unsentRequests;
522+
List<Map<String, Object>> requestsToSend;
523+
String jsonEncodedRequestsToSend;
524+
RequestsWithEncoding requestsWithEncoding = new RequestsWithEncoding();
525+
526+
if (fraction < 0.01) { //base case
527+
unsentRequests = new ArrayList<>(0);
528+
requestsToSend = new ArrayList<>(0);
529+
} else {
530+
unsentRequests = getUnsentRequests(fraction);
531+
requestsToSend = removeIrrelevantBackgroundStartRequests(unsentRequests);
532+
}
521533

522-
requestsWithEncoding.unsentRequests = unsentRequests;
523-
requestsWithEncoding.requestsToSend = requestsToSend;
524-
requestsWithEncoding.jsonEncodedString = jsonEncodedRequestsToSend;
534+
jsonEncodedRequestsToSend = jsonEncodeUnsentRequests(unsentRequests);
535+
requestsWithEncoding.unsentRequests = unsentRequests;
536+
requestsWithEncoding.requestsToSend = requestsToSend;
537+
requestsWithEncoding.jsonEncodedString = jsonEncodedRequestsToSend;
525538

526-
return requestsWithEncoding;
539+
return requestsWithEncoding;
540+
} catch (OutOfMemoryError E) {
541+
// half the requests will need less memory, recursively
542+
return getRequestsWithEncodedStringStoredRequests(0.5 * fraction);
543+
}
527544
}
528545

529546
private RequestsWithEncoding getRequestsWithEncodedString() {
@@ -532,7 +549,7 @@ private RequestsWithEncoding getRequestsWithEncodedString() {
532549
if (localErrors.size() != 0) {
533550
requestsWithEncoding = getRequestsWithEncodedStringForErrors();
534551
} else {
535-
requestsWithEncoding = getRequestsWithEncodedStringStoredRequests();
552+
requestsWithEncoding = getRequestsWithEncodedStringStoredRequests(1.0);
536553
}
537554

538555
return requestsWithEncoding;
@@ -646,7 +663,7 @@ static void deleteSentRequests(int requestsCount) {
646663
}
647664
}
648665

649-
public List<Map<String, Object>> getUnsentRequests() {
666+
public List<Map<String, Object>> getUnsentRequests(double fraction) {
650667
List<Map<String, Object>> requestData;
651668

652669
synchronized (Request.class) {
@@ -655,8 +672,8 @@ public List<Map<String, Object>> getUnsentRequests() {
655672
SharedPreferences preferences = context.getSharedPreferences(
656673
LEANPLUM, Context.MODE_PRIVATE);
657674
SharedPreferences.Editor editor = preferences.edit();
658-
659-
requestData = LeanplumEventDataManager.getEvents(MAX_EVENTS_PER_API_CALL);
675+
int count = (int) (fraction * MAX_EVENTS_PER_API_CALL);
676+
requestData = LeanplumEventDataManager.getEvents(count);
660677
editor.remove(Constants.Defaults.UUID_KEY);
661678
SharedPreferencesUtil.commitChanges(editor);
662679
}

AndroidSDKTests/src/test/java/com/leanplum/LeanplumTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,11 @@ public void testCrashes() throws Exception {
403403

404404
request1.sendEventually();
405405
request2.sendEventually();
406+
407+
final double fraction = 1.0;
406408
// Get a number of events in the database.
407409
// Expectation: 2 events.
408-
List unsentRequests = request1.getUnsentRequests();
410+
List unsentRequests = request1.getUnsentRequests(fraction);
409411
assertNotNull(unsentRequests);
410412
assertEquals(2, unsentRequests.size());
411413

@@ -427,7 +429,7 @@ public void testCrashes() throws Exception {
427429

428430
// Get a number of events in the database. Checks if ours two events still here.
429431
// Expectation: 2 events.
430-
unsentRequests = request1.getUnsentRequests();
432+
unsentRequests = request1.getUnsentRequests(fraction);
431433
assertNotNull(unsentRequests);
432434
assertEquals(2, unsentRequests.size());
433435

@@ -444,7 +446,7 @@ public void onRequest(String httpMethod, String apiMethod, Map<String, Object> p
444446

445447
// Get a number of events in the database. Make sure we sent all events.
446448
// Expectation: 0 events.
447-
unsentRequests = request1.getUnsentRequests();
449+
unsentRequests = request1.getUnsentRequests(fraction);
448450
assertNotNull(unsentRequests);
449451
assertEquals(0, unsentRequests.size());
450452

AndroidSDKTests/src/test/java/com/leanplum/internal/RequestTest.java

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import java.util.List;
4545
import java.util.Map;
4646

47+
import static org.mockito.AdditionalMatchers.not;
48+
import static org.mockito.Matchers.eq;
4749
import static org.mockito.Mockito.spy;
4850
import static org.mockito.Mockito.when;
4951

@@ -96,14 +98,14 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
9698

9799
// Invoke method with specific test data.
98100
// Expectation: No request returned.
99-
List unsentRequests = request.getUnsentRequests();
101+
List unsentRequests = request.getUnsentRequests(1.0);
100102
assertNotNull(unsentRequests);
101103
assertEquals(0, unsentRequests.size());
102104

103105
// Regular start request.
104106
// Expectation: One request returned.
105107
request.sendEventually();
106-
unsentRequests = request.getUnsentRequests();
108+
unsentRequests = request.getUnsentRequests(1.0);
107109
assertNotNull(unsentRequests);
108110
assertEquals(1, unsentRequests.size());
109111
Request.deleteSentRequests(unsentRequests.size());
@@ -118,7 +120,7 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
118120
put(Constants.Params.BACKGROUND, Boolean.toString(false));
119121
put("fg", "2");
120122
}}).sendEventually();
121-
unsentRequests = request.getUnsentRequests();
123+
unsentRequests = request.getUnsentRequests(1.0);
122124
assertNotNull(unsentRequests);
123125
assertEquals(2, unsentRequests.size());
124126
Request.deleteSentRequests(unsentRequests.size());
@@ -133,7 +135,7 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
133135
put(Constants.Params.BACKGROUND, Boolean.toString(false));
134136
put("fg", "1");
135137
}}).sendEventually();
136-
unsentRequests = request.getUnsentRequests();
138+
unsentRequests = request.getUnsentRequests(1.0);
137139
List unsentRequestsData =
138140
(List) removeIrrelevantBackgroundStartRequests.invoke(Request.class, unsentRequests);
139141
assertNotNull(unsentRequestsData);
@@ -155,7 +157,7 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
155157
put(Constants.Params.BACKGROUND, Boolean.toString(false));
156158
put("fg", "1");
157159
}}).sendEventually();
158-
unsentRequests = request.getUnsentRequests();
160+
unsentRequests = request.getUnsentRequests(1.0);
159161

160162
assertNotNull(unsentRequests);
161163
unsentRequestsData =
@@ -178,7 +180,7 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
178180
put(Constants.Params.BACKGROUND, Boolean.toString(true));
179181
put("bg", "2");
180182
}}).sendEventually();
181-
unsentRequests = request.getUnsentRequests();
183+
unsentRequests = request.getUnsentRequests(1.0);
182184
assertNotNull(unsentRequests);
183185
unsentRequestsData =
184186
(List) removeIrrelevantBackgroundStartRequests.invoke(Request.class, unsentRequests);
@@ -200,7 +202,7 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
200202
put(Constants.Params.BACKGROUND, Boolean.toString(true));
201203
put("bg", "2");
202204
}}).sendEventually();
203-
unsentRequests = request.getUnsentRequests();
205+
unsentRequests = request.getUnsentRequests(1.0);
204206
unsentRequestsData =
205207
(List) removeIrrelevantBackgroundStartRequests.invoke(Request.class, unsentRequests);
206208
assertNotNull(unsentRequestsData);
@@ -209,18 +211,92 @@ public void testRemoveIrrelevantBackgroundStartRequests() throws NoSuchMethodExc
209211
LeanplumEventDataManagerTest.setDatabaseToNull();
210212
}
211213

214+
// Given a list of 5000 that generate OOM exceptions
215+
// we want to generate the requests to send
216+
// The list should try and get a smaller fraction of the available requests
217+
@Test
218+
public void testJsonEncodeUnsentRequestsWithExceptionLargeNumbers() throws NoSuchMethodException,
219+
InvocationTargetException, IllegalAccessException {
220+
LeanplumEventDataManager.init(Leanplum.getContext());
221+
Request.RequestsWithEncoding requestsWithEncoding;
222+
// Prepare testable objects and method.
223+
Request request = spy(new Request("POST", Constants.Methods.START, null));
224+
request.sendEventually(); // first request added
225+
for (int i = 0;i < 4999; i++) { // remaining requests to make up 5000
226+
new Request("POST", Constants.Methods.START, null).sendEventually();
227+
}
228+
// Expectation: 5000 requests returned.
229+
requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests(1.0);
230+
231+
assertNotNull(requestsWithEncoding.unsentRequests);
232+
assertNotNull(requestsWithEncoding.requestsToSend);
233+
assertNotNull(requestsWithEncoding.jsonEncodedString);
234+
assertEquals(5000, requestsWithEncoding.unsentRequests.size());
235+
236+
// Throw OOM on 5000 requests
237+
// Expectation: 2500 requests returned.
238+
when(request.getUnsentRequests(1.0)).thenThrow(OutOfMemoryError.class);
239+
requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests(1.0);
240+
241+
assertNotNull(requestsWithEncoding.unsentRequests);
242+
assertNotNull(requestsWithEncoding.requestsToSend);
243+
assertNotNull(requestsWithEncoding.jsonEncodedString);
244+
assertEquals(2500, requestsWithEncoding.unsentRequests.size());
245+
246+
// Throw OOM on 2500, 5000 requests
247+
// Expectation: 1250 requests returned.
248+
when(request.getUnsentRequests(0.5)).thenThrow(OutOfMemoryError.class);
249+
requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests(1.0);
250+
assertEquals(1250, requestsWithEncoding.unsentRequests.size());
251+
252+
// Throw OOM on serializing any finite number of requests (extreme condition)
253+
// Expectation: Determine only 0 requests to be sent
254+
when(request.getUnsentRequests(not(eq(0)))).thenThrow(OutOfMemoryError.class);
255+
requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests(1.0);
256+
257+
assertNotNull(requestsWithEncoding.unsentRequests);
258+
assertNotNull(requestsWithEncoding.requestsToSend);
259+
assertNotNull(requestsWithEncoding.jsonEncodedString);
260+
assertEquals(0, requestsWithEncoding.unsentRequests.size());
261+
assertEquals(0, requestsWithEncoding.requestsToSend.size());
262+
assertEquals("{\"data\":[]}", requestsWithEncoding.jsonEncodedString);
263+
264+
}
265+
266+
// Given a list of unsent requests that generate an OOM exception
267+
// we want to generate the requests to send
268+
// The list should try and get a smaller fraction of the available requests
269+
@Test
270+
public void testJsonEncodeUnsentRequestsWithException() {
271+
List<Map<String, Object>> requests = mockRequests(4);
272+
273+
Request realRequest = new Request("POST", Constants.Methods.START, null);
274+
Request request = spy(realRequest);
275+
when(request.getUnsentRequests(1.0)).thenThrow(OutOfMemoryError.class);
276+
when(request.getUnsentRequests(0.5)).thenThrow(OutOfMemoryError.class);
277+
when(request.getUnsentRequests(0.25)).thenReturn(requests);
278+
279+
Request.RequestsWithEncoding requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests(1.0);
280+
281+
assertEquals(4, requestsWithEncoding .unsentRequests.size());
282+
assertEquals(4, requestsWithEncoding .requestsToSend.size());
283+
final String expectedJson = "{\"data\":[{\"0\":\"testData\"},{\"1\":\"testData\"},{\"2\":\"testData\"},{\"3\":\"testData\"}]}";
284+
assertEquals(expectedJson, requestsWithEncoding.jsonEncodedString);
285+
}
286+
212287
// Given a list of unsent requests
213-
// we want to get the list of strings to send
214-
// We should get back the correct list of requests to send
288+
// we want to generate the requests to send
289+
// The String should have the expected format, and the request count should be equal to the
290+
// number of unsent requests
215291
@Test
216292
public void testJsonEncodeUnsentRequests() {
217293
List<Map<String, Object>> requests = mockRequests(4);
218294

219295
Request realRequest = new Request("POST", Constants.Methods.START, null);
220296
Request request = spy(realRequest);
221-
when(request.getUnsentRequests()).thenReturn(requests);
297+
when(request.getUnsentRequests(1.0)).thenReturn(requests);
222298

223-
Request.RequestsWithEncoding requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests();
299+
Request.RequestsWithEncoding requestsWithEncoding = request.getRequestsWithEncodedStringStoredRequests(1.0);
224300

225301
assertEquals(4, requestsWithEncoding .unsentRequests.size());
226302
assertEquals(4, requestsWithEncoding .requestsToSend.size());

0 commit comments

Comments
 (0)