Skip to content

Commit 4e17065

Browse files
authored
Refactor batching of requests (#425)
* Remove synchronization * Add batch factory * Use batch factory * Add RequestBatch class * Fix unit tests * Move unit tests to correct file * Move all UUID code to helper class * Rename field * Re-use uuid class for errors * Remove 'shared prefs' to 'sqlite' migration code
1 parent 7462892 commit 4e17065

File tree

12 files changed

+785
-649
lines changed

12 files changed

+785
-649
lines changed

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

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017, Leanplum, Inc. All rights reserved.
2+
* Copyright 2020, Leanplum, Inc. All rights reserved.
33
*
44
* Licensed to the Apache Software Foundation (ASF) under one
55
* or more contributor license agreements. See the NOTICE file
@@ -23,24 +23,19 @@
2323

2424
import android.content.ContentValues;
2525
import android.content.Context;
26-
import android.content.SharedPreferences;
2726
import android.database.Cursor;
2827

2928
import android.database.DatabaseUtils;
3029
import android.database.sqlite.SQLiteDatabase;
3130
import android.database.sqlite.SQLiteOpenHelper;
3231

3332
import com.leanplum.Leanplum;
34-
import com.leanplum.utils.SharedPreferencesUtil;
3533

36-
import org.json.JSONException;
3734
import org.json.JSONObject;
3835

3936
import java.util.ArrayList;
4037
import java.util.List;
41-
import java.util.Locale;
4238
import java.util.Map;
43-
import java.util.UUID;
4439

4540
/**
4641
* LeanplumEventDataManager class to work with SQLite.
@@ -204,71 +199,12 @@ public void onCreate(SQLiteDatabase db) {
204199
// Create event table.
205200
db.execSQL("CREATE TABLE IF NOT EXISTS " + EVENT_TABLE_NAME + "(" + COLUMN_DATA +
206201
" TEXT)");
207-
208-
// Migrate old data from shared preferences.
209-
try {
210-
migrateFromSharedPreferences(db);
211-
} catch (Throwable t) {
212-
Log.e("Cannot move old data from shared preferences to SQLite table.", t);
213-
Log.exception(t);
214-
}
215202
}
216203

217204
@Override
218205
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
219206
// No used for now.
220207
}
221208

222-
/**
223-
* Migrate data from shared preferences to SQLite.
224-
*/
225-
private static void migrateFromSharedPreferences(SQLiteDatabase db) {
226-
synchronized (Request.class) {
227-
Context context = Leanplum.getContext();
228-
SharedPreferences preferences = context.getSharedPreferences(
229-
Constants.Defaults.LEANPLUM, Context.MODE_PRIVATE);
230-
SharedPreferences.Editor editor = preferences.edit();
231-
int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
232-
if (count == 0) {
233-
return;
234-
}
235-
236-
List<Map<String, Object>> requestData = new ArrayList<>();
237-
for (int i = 0; i < count; i++) {
238-
String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i);
239-
Map<String, Object> requestArgs;
240-
try {
241-
requestArgs = JsonConverter.mapFromJson(new JSONObject(
242-
preferences.getString(itemKey, "{}")));
243-
requestData.add(requestArgs);
244-
} catch (JSONException e) {
245-
e.printStackTrace();
246-
}
247-
editor.remove(itemKey);
248-
}
249-
250-
editor.remove(Constants.Defaults.COUNT_KEY);
251-
252-
ContentValues contentValues = new ContentValues();
253-
254-
try {
255-
String uuid = preferences.getString(Constants.Defaults.UUID_KEY, null);
256-
if (uuid == null || count % RequestSender.MAX_EVENTS_PER_API_CALL == 0) {
257-
uuid = UUID.randomUUID().toString();
258-
editor.putString(Constants.Defaults.UUID_KEY, uuid);
259-
}
260-
for (Map<String, Object> event : requestData) {
261-
event.put(Constants.Params.UUID, uuid);
262-
contentValues.put(COLUMN_DATA, JsonConverter.toJson(event));
263-
db.insert(EVENT_TABLE_NAME, null, contentValues);
264-
contentValues.clear();
265-
}
266-
SharedPreferencesUtil.commitChanges(editor);
267-
} catch (Throwable t) {
268-
Log.e("Failed on migration data from shared preferences.", t);
269-
Log.exception(t);
270-
}
271-
}
272-
}
273209
}
274210
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2020, Leanplum, Inc. All rights reserved.
3+
*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
package com.leanplum.internal;
23+
24+
import androidx.annotation.NonNull;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
/**
29+
* This class wraps the unsent requests, requests that we need to send
30+
* and the JSON encoded string. Wrapping it in the class allows us to
31+
* retain consistency in the requests we are sending and the actual
32+
* JSON string.
33+
*/
34+
public class RequestBatch {
35+
// all persisted requests
36+
List<Map<String, Object>> requests;
37+
// filtered requests that will be sent
38+
List<Map<String, Object>> requestsToSend;
39+
String jsonEncoded;
40+
41+
public RequestBatch(
42+
@NonNull List<Map<String, Object>> requests,
43+
@NonNull List<Map<String, Object>> requestsToSend,
44+
@NonNull String jsonEncoded) {
45+
this.requests = requests;
46+
this.requestsToSend = requestsToSend;
47+
this.jsonEncoded = jsonEncoded;
48+
}
49+
50+
public int getEventsCount() {
51+
return requests.size();
52+
}
53+
54+
public boolean isEmpty() {
55+
return requestsToSend.isEmpty();
56+
}
57+
58+
public boolean isFull() {
59+
return getEventsCount() == RequestBatchFactory.MAX_EVENTS_PER_API_CALL;
60+
}
61+
62+
public String getJson() {
63+
return jsonEncoded;
64+
}
65+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2020, Leanplum, Inc. All rights reserved.
3+
*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
package com.leanplum.internal;
23+
24+
import android.os.Build;
25+
import androidx.annotation.NonNull;
26+
import androidx.annotation.VisibleForTesting;
27+
import java.util.ArrayList;
28+
import java.util.HashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.UUID;
32+
33+
public class RequestBatchFactory {
34+
35+
static final int MAX_EVENTS_PER_API_CALL = (Build.VERSION.SDK_INT <= 17) ? 5000 : 10000;
36+
37+
private final RequestUuidHelper uuidHelper = new RequestUuidHelper();
38+
39+
/**
40+
* In the presence of errors we do not send any events but only the errors.
41+
*/
42+
public RequestBatch createErrorBatch(List<Map<String, Object>> localErrors) {
43+
List<Map<String, Object>> requests = new ArrayList<>();
44+
String jsonEncodedRequestsToSend;
45+
46+
uuidHelper.attachNewUuid(localErrors);
47+
jsonEncodedRequestsToSend = jsonEncodeRequests(requests);
48+
49+
// for errors, we send all unsent requests so they are identical
50+
return new RequestBatch(requests, requests, jsonEncodedRequestsToSend);
51+
}
52+
53+
/**
54+
* Creates batch with all saved events with count of up to {@link #MAX_EVENTS_PER_API_CALL}.
55+
*/
56+
public RequestBatch createNextBatch() {
57+
return createNextBatch(1.0);
58+
}
59+
60+
/**
61+
* @param fraction Decimal from 0 to 1. It says what part of all saved events to include in batch.
62+
*/
63+
@VisibleForTesting
64+
protected RequestBatch createNextBatch(double fraction) {
65+
try {
66+
List<Map<String, Object>> requests;
67+
List<Map<String, Object>> requestsToSend;
68+
69+
if (fraction < 0.01) { //base case
70+
requests = new ArrayList<>(0);
71+
requestsToSend = new ArrayList<>(0);
72+
} else {
73+
requests = getUnsentRequests(fraction);
74+
requestsToSend = removeIrrelevantBackgroundStartRequests(requests);
75+
}
76+
77+
String jsonEncoded = jsonEncodeRequests(requestsToSend);
78+
79+
return new RequestBatch(requests, requestsToSend, jsonEncoded);
80+
} catch (OutOfMemoryError oom) {
81+
// half the requests will need less memory, recursively
82+
return createNextBatch(0.5 * fraction);
83+
}
84+
}
85+
86+
/**
87+
* In various scenarios we can end up batching a big number of requests (e.g. device is offline,
88+
* background sessions), which could make the stored API calls batch look something like:
89+
* <p>
90+
* <code>start(B), start(B), start(F), track, start(B), track, start(F), resumeSession</code>
91+
* <p>
92+
* where <code>start(B)</code> indicates a start in the background, and <code>start(F)</code>
93+
* one in the foreground.
94+
* <p>
95+
* In this case the first two <code>start(B)</code> can be dropped because they don't contribute
96+
* any relevant information for the batch call.
97+
* <p>
98+
* Essentially we drop every <code>start(B)</code> call, that is directly followed by any kind of
99+
* a <code>start</code> call.
100+
*
101+
* @param requestData A list of the requests, stored on the device.
102+
* @return A list of only these requests, which contain relevant information for the API call.
103+
*/
104+
@VisibleForTesting
105+
protected List<Map<String, Object>> removeIrrelevantBackgroundStartRequests(
106+
List<Map<String, Object>> requestData) {
107+
List<Map<String, Object>> relevantRequests = new ArrayList<>();
108+
109+
int requestCount = requestData.size();
110+
if (requestCount > 0) {
111+
for (int i = 0; i < requestCount; i++) {
112+
Map<String, Object> currentRequest = requestData.get(i);
113+
if (i < requestCount - 1
114+
&& RequestBuilder.ACTION_START.equals(requestData.get(i + 1).get(Constants.Params.ACTION))
115+
&& RequestBuilder.ACTION_START.equals(currentRequest.get(Constants.Params.ACTION))
116+
&& Boolean.TRUE.toString().equals(currentRequest.get(Constants.Params.BACKGROUND))) {
117+
continue;
118+
}
119+
relevantRequests.add(currentRequest);
120+
}
121+
}
122+
123+
return relevantRequests;
124+
}
125+
126+
@VisibleForTesting
127+
public List<Map<String, Object>> getUnsentRequests(double fraction) {
128+
int eventsCount = (int) (fraction * MAX_EVENTS_PER_API_CALL);
129+
List<Map<String, Object>> events =
130+
LeanplumEventDataManager.sharedInstance().getEvents(eventsCount);
131+
132+
// start new batch id for subsequent requests
133+
uuidHelper.deleteUuid();
134+
135+
// if less than 100% of requests are sent the uuid will be reset
136+
if (fraction < 1) {
137+
// Check PR#340. This is only if OutOfMemoryException is thrown.
138+
uuidHelper.attachNewUuid(events);
139+
}
140+
return events;
141+
}
142+
143+
@VisibleForTesting
144+
protected String jsonEncodeRequests(List<Map<String, Object>> requestData) {
145+
Map<String, Object> data = new HashMap<>();
146+
data.put(Constants.Params.DATA, requestData);
147+
return JsonConverter.toJson(data);
148+
}
149+
150+
public void deleteFinishedBatch(@NonNull RequestBatch batch) {
151+
// Currently no enumeration of the requests so removing the first ones in the queue
152+
int eventsCount = batch.getEventsCount();
153+
if (eventsCount == 0) {
154+
return;
155+
}
156+
LeanplumEventDataManager.sharedInstance().deleteEvents(eventsCount);
157+
}
158+
159+
}

0 commit comments

Comments
 (0)