Skip to content

Commit ada935e

Browse files
Merge pull request #201 from Leanplum/feat/1355
2 parents 057ec34 + b566fb2 commit ada935e

File tree

3 files changed

+142
-0
lines changed

3 files changed

+142
-0
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class Request {
5959
private static final long DEVELOPMENT_MIN_DELAY_MS = 100;
6060
private static final long DEVELOPMENT_MAX_DELAY_MS = 5000;
6161
private static final long PRODUCTION_DELAY = 60000;
62+
private RequestSequenceRecorder requestSequenceRecorder;
6263
static final int MAX_EVENTS_PER_API_CALL;
6364
static final String LEANPLUM = "__leanplum__";
6465
static final String UUID_KEY = "uuid";
@@ -164,6 +165,28 @@ public static void saveToken() {
164165
SharedPreferencesUtil.commitChanges(editor);
165166
}
166167

168+
private static class NoRequestSequenceRecorder implements RequestSequenceRecorder {
169+
@Override
170+
public void beforeRead() {
171+
// No op.
172+
}
173+
174+
@Override
175+
public void afterRead() {
176+
// No op.
177+
}
178+
179+
@Override
180+
public void beforeWrite() {
181+
// No op.
182+
}
183+
184+
@Override
185+
public void afterWrite() {
186+
// No op.
187+
}
188+
}
189+
167190
public static String appId() {
168191
return appId;
169192
}
@@ -177,6 +200,10 @@ public static String userId() {
177200
}
178201

179202
public Request(String httpMethod, String apiMethod, Map<String, Object> params) {
203+
this(httpMethod, apiMethod, params, new NoRequestSequenceRecorder());
204+
}
205+
206+
Request(String httpMethod, String apiMethod, Map<String, Object> params, RequestSequenceRecorder requestSequenceRecorder) {
180207
this.httpMethod = httpMethod;
181208
this.apiMethod = apiMethod;
182209
this.params = params != null ? params : new HashMap<String, Object>();
@@ -187,6 +214,7 @@ public Request(String httpMethod, String apiMethod, Map<String, Object> params)
187214
// Make sure the Handler is initialized on the main thread.
188215
OsHandler.getInstance();
189216
dataBaseIndex = -1;
217+
this.requestSequenceRecorder = requestSequenceRecorder;
190218
}
191219

192220
public static Request get(String apiMethod, Map<String, Object> params) {
@@ -232,6 +260,8 @@ private Map<String, Object> createArgsDictionary() {
232260

233261
private void saveRequestForLater(Map<String, Object> args) {
234262
try {
263+
requestSequenceRecorder.beforeWrite();
264+
235265
synchronized (Request.class) {
236266
Context context = Leanplum.getContext();
237267
SharedPreferences preferences = context.getSharedPreferences(
@@ -255,6 +285,8 @@ private void saveRequestForLater(Map<String, Object> args) {
255285
eventCallbackManager.addCallbacks(this, response, error);
256286
}
257287
}
288+
289+
requestSequenceRecorder.afterWrite();
258290
} catch (Throwable t) {
259291
Util.handleException(t);
260292
}
@@ -549,8 +581,12 @@ private RequestsWithEncoding getRequestsWithEncodedString() {
549581
}
550582

551583
private void sendRequests() {
584+
requestSequenceRecorder.beforeRead();
585+
552586
RequestsWithEncoding requestsWithEncoding = getRequestsWithEncodedString();
553587

588+
requestSequenceRecorder.afterRead();
589+
554590
List<Map<String, Object>> unsentRequests = requestsWithEncoding.unsentRequests;
555591
List<Map<String, Object>> requestsToSend = requestsWithEncoding.requestsToSend;
556592
String jsonEncodedString = requestsWithEncoding.jsonEncodedString;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.leanplum.internal;
2+
3+
/** Records request call sequence of read/write operations to database. */
4+
public interface RequestSequenceRecorder {
5+
/** Executes before database read in Request. */
6+
void beforeRead();
7+
8+
/** Executes after database read in Request. */
9+
void afterRead();
10+
11+
/** Executes before database write in Request. */
12+
void beforeWrite();
13+
14+
/** Executes after database write in Request. */
15+
void afterWrite();
16+
}

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@
3737

3838
import java.lang.reflect.InvocationTargetException;
3939
import java.lang.reflect.Method;
40+
import java.time.Instant;
4041
import java.util.ArrayList;
4142
import java.util.HashMap;
4243
import java.util.List;
4344
import java.util.Map;
45+
import java.util.concurrent.Semaphore;
46+
import java.util.concurrent.TimeUnit;
4447

4548
import static org.mockito.AdditionalMatchers.not;
4649
import static org.mockito.Matchers.eq;
@@ -57,6 +60,7 @@
5760
)
5861
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "org.json.*", "org.powermock.*"})
5962
public class RequestTest extends TestCase {
63+
public final String POST = "POST";
6064
/**
6165
* Runs before every test case.
6266
*/
@@ -67,6 +71,42 @@ public void setUp() {
6771
Leanplum.setApplicationContext(context);
6872
}
6973

74+
/** Test that read writes happened sequentially when calling sendNow(). */
75+
@Test
76+
public void shouldWriteRequestAndSendInSequence() throws InterruptedException {
77+
// Given a request.
78+
Map<String, Object> params = new HashMap<>();
79+
params.put("data1", "value1");
80+
params.put("data2", "value2");
81+
final ThreadRequestSequenceRecorder threadRequestSequenceRecorder =
82+
new ThreadRequestSequenceRecorder();
83+
Request request =
84+
new Request(POST, Constants.Methods.START, params, threadRequestSequenceRecorder);
85+
request.setAppId("fskadfshdbfa", "wee5w4waer422323");
86+
87+
new Thread(
88+
new Runnable() {
89+
@Override
90+
public void run() {
91+
try {
92+
Thread.sleep(100);
93+
} catch (InterruptedException e) {
94+
throw new RuntimeException(e);
95+
}
96+
threadRequestSequenceRecorder.writeSemaphore.release(1);
97+
}
98+
})
99+
.start();
100+
101+
// When the request is sent.
102+
request.sendIfConnected();
103+
104+
threadRequestSequenceRecorder.testThreadSemaphore.tryAcquire(5000, TimeUnit.MILLISECONDS);
105+
106+
// Then the request is written to the local db first, and then read and sent.
107+
threadRequestSequenceRecorder.assertCallSequence();
108+
}
109+
70110
/**
71111
* Tests the testRemoveIrrelevantBackgroundStartRequests method.
72112
* <p>
@@ -323,4 +363,54 @@ private List<Map<String, Object>> mockRequests(int requestSize) {
323363
return requests;
324364
}
325365

366+
private static class ThreadRequestSequenceRecorder implements RequestSequenceRecorder {
367+
Instant beforeReadTime, afterReadTime, beforeWriteTime, afterWriteTime;
368+
final Semaphore writeSemaphore = new Semaphore(0);
369+
final Semaphore readSemaphore = new Semaphore(1);
370+
final Semaphore testThreadSemaphore = new Semaphore(0);
371+
372+
@Override
373+
public void beforeRead() {
374+
try {
375+
readSemaphore.tryAcquire(10, TimeUnit.SECONDS);
376+
beforeReadTime = Instant.now();
377+
} catch (InterruptedException e) {
378+
throw new RuntimeException(e);
379+
} finally {
380+
readSemaphore.release();
381+
}
382+
}
383+
384+
@Override
385+
public void afterRead() {
386+
afterReadTime = Instant.now();
387+
testThreadSemaphore.release(1);
388+
}
389+
390+
@Override
391+
public void beforeWrite() {
392+
// since we are blocking on main thread
393+
try {
394+
writeSemaphore.tryAcquire(10, TimeUnit.SECONDS);
395+
Thread.sleep(2000);
396+
} catch (InterruptedException e) {
397+
throw new RuntimeException(e);
398+
} finally {
399+
writeSemaphore.release();
400+
}
401+
beforeWriteTime = Instant.now();
402+
}
403+
404+
@Override
405+
public void afterWrite() {
406+
afterWriteTime = Instant.now();
407+
}
408+
409+
void assertCallSequence() {
410+
assertTrue(
411+
beforeWriteTime.isBefore(afterWriteTime)
412+
&& beforeReadTime.isBefore(afterReadTime)
413+
&& beforeReadTime.isAfter(afterWriteTime));
414+
}
415+
}
326416
}

0 commit comments

Comments
 (0)