Skip to content

Commit 353c95a

Browse files
Daniel Cochranfacebook-github-bot
authored andcommitted
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. facebook/react-native#16905 [Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely. Pull Request resolved: facebook/react-native#18522 Differential Revision: D8624088 Pulled By: hramos fbshipit-source-id: a1d2e3458d98467845cb34ac73f2aafaaa15ace2
1 parent 35731af commit 353c95a

File tree

1 file changed

+49
-6
lines changed

1 file changed

+49
-6
lines changed

AsyncStorageModule.java

Lines changed: 49 additions & 6 deletions
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
/**

0 commit comments

Comments
 (0)