Skip to content

Commit 1b09bd7

Browse files
Daniel Cochranfacebook-github-bot
Daniel Cochran
authored andcommittedJul 30, 2018
make AsyncStorage serially execute requests (#18522)
Summary: This patch is a bit of a hack job, but I'd argue it's necessary to dramatically improve the dev UX on Android devices. Somewhere in react-native, there's a shared SerialExecutor which AsyncStorage uses that is getting blocked, causing remote debugging to occasionally hang indefinitely for folks making AsyncStorage requests. This is frustrating from a dev UX perspective, and has persisted across several versions as far back as RN 0.44, and still remains on RN 0.54. The issue seems to only happen on Android > 7+, which is likely because the ThreadPoolExecutor behavior changed in this version: https://stackoverflow.com/questions/9654148/android-asynctask-threads-limits Fixes #14101 We've been using this patch in production for the past 4 months on our team by overriding the AsyncStorage native module. We use AsyncStorage extensively for offline data and caching. You can test by compiling this commit version into a test react native repository that is set to build from source: ```sh git clone https://github.com/dannycochran/react-native rnAsyncStorage cd rnAsyncStorage git checkout asyncStorage cd .. git clone https://github.com/dannycochran/asyncStorageTest yarn install cp -r ../rnAsyncStorage node_modules/react-native react-native run-android ``` No documentation change is required. #16905 [Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely. Pull Request resolved: #18522 Differential Revision: D8624088 Pulled By: hramos fbshipit-source-id: a1d2e3458d98467845cb34ac73f2aafaaa15ace2
1 parent 82af7c9 commit 1b09bd7

File tree

2 files changed

+82
-22
lines changed

2 files changed

+82
-22
lines changed
 

‎ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java

+49-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77

88
package com.facebook.react.modules.storage;
99

10+
import java.util.ArrayDeque;
1011
import java.util.HashSet;
12+
import java.util.concurrent.Executor;
1113

1214
import android.database.Cursor;
1315
import android.database.sqlite.SQLiteStatement;
16+
import android.os.AsyncTask;
1417

1518
import com.facebook.common.logging.FLog;
1619
import com.facebook.react.bridge.Arguments;
@@ -23,6 +26,7 @@
2326
import com.facebook.react.bridge.WritableArray;
2427
import com.facebook.react.bridge.WritableMap;
2528
import com.facebook.react.common.ReactConstants;
29+
import com.facebook.react.common.annotations.VisibleForTesting;
2630
import com.facebook.react.module.annotations.ReactModule;
2731
import com.facebook.react.modules.common.ModuleDataCleaner;
2832

@@ -43,8 +47,47 @@ public final class AsyncStorageModule
4347
private ReactDatabaseSupplier mReactDatabaseSupplier;
4448
private boolean mShuttingDown = false;
4549

50+
// Adapted from https://android.googlesource.com/platform/frameworks/base.git/+/1488a3a19d4681a41fb45570c15e14d99db1cb66/core/java/android/os/AsyncTask.java#237
51+
private class SerialExecutor implements Executor {
52+
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
53+
private Runnable mActive;
54+
private final Executor executor;
55+
56+
SerialExecutor(Executor executor) {
57+
this.executor = executor;
58+
}
59+
60+
public synchronized void execute(final Runnable r) {
61+
mTasks.offer(new Runnable() {
62+
public void run() {
63+
try {
64+
r.run();
65+
} finally {
66+
scheduleNext();
67+
}
68+
}
69+
});
70+
if (mActive == null) {
71+
scheduleNext();
72+
}
73+
}
74+
synchronized void scheduleNext() {
75+
if ((mActive = mTasks.poll()) != null) {
76+
executor.execute(mActive);
77+
}
78+
}
79+
}
80+
81+
private final SerialExecutor executor;
82+
4683
public AsyncStorageModule(ReactApplicationContext reactContext) {
84+
this(reactContext, AsyncTask.THREAD_POOL_EXECUTOR);
85+
}
86+
87+
@VisibleForTesting
88+
AsyncStorageModule(ReactApplicationContext reactContext, Executor executor) {
4789
super(reactContext);
90+
this.executor = new SerialExecutor(executor);
4891
mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext);
4992
}
5093

@@ -141,7 +184,7 @@ protected void doInBackgroundGuarded(Void... params) {
141184

142185
callback.invoke(null, data);
143186
}
144-
}.execute();
187+
}.executeOnExecutor(executor);
145188
}
146189

147190
/**
@@ -208,7 +251,7 @@ protected void doInBackgroundGuarded(Void... params) {
208251
callback.invoke();
209252
}
210253
}
211-
}.execute();
254+
}.executeOnExecutor(executor);
212255
}
213256

214257
/**
@@ -259,7 +302,7 @@ protected void doInBackgroundGuarded(Void... params) {
259302
callback.invoke();
260303
}
261304
}
262-
}.execute();
305+
}.executeOnExecutor(executor);
263306
}
264307

265308
/**
@@ -322,7 +365,7 @@ protected void doInBackgroundGuarded(Void... params) {
322365
callback.invoke();
323366
}
324367
}
325-
}.execute();
368+
}.executeOnExecutor(executor);
326369
}
327370

328371
/**
@@ -345,7 +388,7 @@ protected void doInBackgroundGuarded(Void... params) {
345388
callback.invoke(AsyncStorageErrorUtil.getError(null, e.getMessage()));
346389
}
347390
}
348-
}.execute();
391+
}.executeOnExecutor(executor);
349392
}
350393

351394
/**
@@ -379,7 +422,7 @@ protected void doInBackgroundGuarded(Void... params) {
379422
}
380423
callback.invoke(null, data);
381424
}
382-
}.execute();
425+
}.executeOnExecutor(executor);
383426
}
384427

385428
/**

‎ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java

+33-16
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import java.util.Arrays;
1111
import java.util.HashMap;
1212
import java.util.Map;
13+
import java.util.concurrent.Executor;
1314

1415
import android.app.Activity;
1516
import android.content.Context;
1617
import android.content.ContextWrapper;
18+
import android.os.AsyncTask;
1719

1820
import com.facebook.react.bridge.Arguments;
1921
import com.facebook.react.bridge.Callback;
22+
import com.facebook.react.bridge.GuardedAsyncTask;
2023
import com.facebook.react.bridge.ReactApplicationContext;
2124
import com.facebook.react.bridge.ReactTestHelper;
2225
import com.facebook.react.bridge.JavaOnlyArray;
@@ -35,14 +38,15 @@
3538
import org.mockito.Mockito;
3639
import org.mockito.invocation.InvocationOnMock;
3740
import org.mockito.stubbing.Answer;
41+
import org.mockito.verification.VerificationMode;
3842
import org.powermock.api.mockito.PowerMockito;
3943
import org.powermock.core.classloader.annotations.PrepareForTest;
4044
import org.powermock.core.classloader.annotations.PowerMockIgnore;
4145
import org.powermock.modules.junit4.rule.PowerMockRule;
4246
import org.robolectric.RuntimeEnvironment;
4347
import org.robolectric.Robolectric;
4448
import org.robolectric.RobolectricTestRunner;
45-
import org.robolectric.annotation.Config;
49+
import org.robolectric.util.concurrent.RoboExecutorService;
4650

4751
import static org.mockito.Mockito.mock;
4852
import static org.fest.assertions.api.Assertions.assertThat;
@@ -81,7 +85,10 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
8185
});
8286

8387
// don't use Robolectric before initializing mocks
84-
mStorage = new AsyncStorageModule(ReactTestHelper.createCatalystContextForTest());
88+
mStorage = new AsyncStorageModule(
89+
ReactTestHelper.createCatalystContextForTest(),
90+
new RoboExecutorService()
91+
);
8592
mEmptyArray = new JavaOnlyArray();
8693
}
8794

@@ -104,15 +111,15 @@ public void testMultiSetMultiGet() {
104111

105112
Callback setCallback = mock(Callback.class);
106113
mStorage.multiSet(keyValues, setCallback);
107-
Mockito.verify(setCallback, Mockito.times(1)).invoke();
114+
verify(setCallback, Mockito.times(1)).invoke();
108115

109116
JavaOnlyArray keys = new JavaOnlyArray();
110117
keys.pushString(key1);
111118
keys.pushString(key2);
112119

113120
Callback getCallback = mock(Callback.class);
114121
mStorage.multiGet(keys, getCallback);
115-
Mockito.verify(getCallback, Mockito.times(1)).invoke(null, keyValues);
122+
verify(getCallback, Mockito.times(1)).invoke(null, keyValues);
116123

117124
keys.pushString(fakeKey);
118125
JavaOnlyArray row3 = new JavaOnlyArray();
@@ -122,7 +129,7 @@ public void testMultiSetMultiGet() {
122129

123130
Callback getCallback2 = mock(Callback.class);
124131
mStorage.multiGet(keys, getCallback2);
125-
Mockito.verify(getCallback2, Mockito.times(1)).invoke(null, keyValues);
132+
verify(getCallback2, Mockito.times(1)).invoke(null, keyValues);
126133
}
127134

128135
@Test
@@ -143,22 +150,22 @@ public void testMultiRemove() {
143150

144151
Callback getCallback = mock(Callback.class);
145152
mStorage.multiRemove(keys, getCallback);
146-
Mockito.verify(getCallback, Mockito.times(1)).invoke();
153+
verify(getCallback, Mockito.times(1)).invoke();
147154

148155
Callback getAllCallback = mock(Callback.class);
149156
mStorage.getAllKeys(getAllCallback);
150-
Mockito.verify(getAllCallback, Mockito.times(1)).invoke(null, mEmptyArray);
157+
verify(getAllCallback, Mockito.times(1)).invoke(null, mEmptyArray);
151158

152159
mStorage.multiSet(keyValues, mock(Callback.class));
153160

154161
keys.pushString("fakeKey");
155162
Callback getCallback2 = mock(Callback.class);
156163
mStorage.multiRemove(keys, getCallback2);
157-
Mockito.verify(getCallback2, Mockito.times(1)).invoke();
164+
verify(getCallback2, Mockito.times(1)).invoke();
158165

159166
Callback getAllCallback2 = mock(Callback.class);
160167
mStorage.getAllKeys(getAllCallback2);
161-
Mockito.verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray);
168+
verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray);
162169
}
163170

164171
@Test
@@ -175,7 +182,7 @@ public void testMultiMerge() throws Exception {
175182
{
176183
Callback callback = mock(Callback.class);
177184
mStorage.multiGet(getArray(mergeKey), callback);
178-
Mockito.verify(callback, Mockito.times(1))
185+
verify(callback, Mockito.times(1))
179186
.invoke(null, JavaOnlyArray.of(getArray(mergeKey, value.toString())));
180187
}
181188

@@ -200,7 +207,7 @@ public void testMultiMerge() throws Exception {
200207
value.put("foo2", createJSONObject("key1", "val3", "key2", "val2"));
201208
Callback callback = mock(Callback.class);
202209
mStorage.multiGet(getArray(mergeKey), callback);
203-
Mockito.verify(callback, Mockito.times(1))
210+
verify(callback, Mockito.times(1))
204211
.invoke(null, JavaOnlyArray.of(getArray(mergeKey, value.toString())));
205212
}
206213

@@ -219,18 +226,18 @@ public void testGetAllKeys() {
219226

220227
Callback getAllCallback = mock(Callback.class);
221228
mStorage.getAllKeys(getAllCallback);
222-
Mockito.verify(getAllCallback, Mockito.times(1)).invoke(null, storedKeys);
229+
verify(getAllCallback, Mockito.times(1)).invoke(null, storedKeys);
223230

224231
Callback getAllCallback2 = mock(Callback.class);
225232
mStorage.multiRemove(getArray(keys[0]), mock(Callback.class));
226233

227234
mStorage.getAllKeys(getAllCallback2);
228-
Mockito.verify(getAllCallback2, Mockito.times(1)).invoke(null, getArray(keys[1]));
235+
verify(getAllCallback2, Mockito.times(1)).invoke(null, getArray(keys[1]));
229236

230237
mStorage.multiRemove(getArray(keys[1]), mock(Callback.class));
231238
Callback getAllCallback3 = mock(Callback.class);
232239
mStorage.getAllKeys(getAllCallback3);
233-
Mockito.verify(getAllCallback3, Mockito.times(1)).invoke(null, mEmptyArray);
240+
verify(getAllCallback3, Mockito.times(1)).invoke(null, mEmptyArray);
234241
}
235242

236243
@Test
@@ -242,11 +249,11 @@ public void testClear() {
242249

243250
Callback clearCallback2 = mock(Callback.class);
244251
mStorage.clear(clearCallback2);
245-
Mockito.verify(clearCallback2, Mockito.times(1)).invoke();
252+
verify(clearCallback2, Mockito.times(1)).invoke();
246253

247254
Callback getAllCallback2 = mock(Callback.class);
248255
mStorage.getAllKeys(getAllCallback2);
249-
Mockito.verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray);
256+
verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray);
250257
}
251258

252259
@Test
@@ -339,4 +346,14 @@ private JavaOnlyArray getArray(String... values) {
339346
}
340347
return array;
341348
}
349+
350+
private static void waitForAsync() {
351+
Robolectric.flushForegroundThreadScheduler();
352+
Robolectric.flushBackgroundThreadScheduler();
353+
}
354+
355+
private static <T> T verify(T mock, VerificationMode mode) {
356+
waitForAsync();
357+
return Mockito.verify(mock, mode);
358+
}
342359
}

2 commit comments

Comments
 (2)

dulmandakh commented on Jul 31, 2018

@dulmandakh
Contributor

I created a cleaner version of running AsyncStorageModule serially, here is the PR #20386.

@hramos please review and merge it if looks feasible

dulmandakh commented on Jul 31, 2018

@dulmandakh
Contributor

Also it still use shared AsyncTask.THREAD_POOL_EXECUTOR, which is the original cause of issues of AsyncStorageModule.

Please sign in to comment.