Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make AsyncStorage serially execute requests #18522

Closed
wants to merge 3 commits into from

Conversation

dannycochran
Copy link
Contributor

Motivation

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

Test Plan

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:

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

Related PRs

No documentation change is required.

#16905

Release Notes

[Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely.

@react-native-bot react-native-bot added the Missing Changelog This PR appears to be missing a changelog, or they are incorrectly formatted. label Mar 23, 2018
@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Mar 23, 2018
@dannycochran
Copy link
Contributor Author

@esprehn @jsdario @shergin

@chirag04
Copy link
Contributor

is this different from SERIAL_EXECUTOR?

@dannycochran
Copy link
Contributor Author

@chirag04 nope, it's literally copied and pasted from there:

https://android.googlesource.com/platform/frameworks/base.git/+/1488a3a19d4681a41fb45570c15e14d99db1cb66/core/java/android/os/AsyncTask.java#237

There's just no way to import it, so I copied and pasted it and left a note in the file.

@dannycochran
Copy link
Contributor Author

@chirag04 can you clarify what your question is? The SerialExecutor class I copied in is the same as the one provided by Android's AsyncTask class.

However, my PR instantiates an instance of SerialExecutor for use with AsyncStorage tasks, because somewhere else in react-native, the shared SerialExecutor class instance is getting blocked when remote debugging. Because the SerialExecutor class is private in AsyncTask, there's no way to import it, hence the copy and paste.

@chirag04
Copy link
Contributor

Because the SerialExecutor class is private in AsyncTask, there's no way to import it, hence the copy and paste.

Yes but an instance of the SerialExecutor class is public(SERIAL_EXECUTOR). So my question was why not use SERIAL_EXECUTOR. I don't understand java well enough but looks like SERIAL_EXECUTOR is the default executor. Also, feel like we are not fixing the root cause here.

@dannycochran
Copy link
Contributor Author

@chirag04 Correct, that's the default, and it's the SerialExecutor instance that's blocked elsewhere in RN. Using that would be identical to the existing behavior. This PR uses its own SerialExecutor instance so it does not get blocked by something else in the codebase.

And yes, this does not get at the root cause, which is that somewhere is posting a task which hangs indefinitely. Still, I would argue landing this is pretty critical to dev UX, if you take a look at the comments / reactions to #14101, you can see how much it's impacting people.

@esprehn
Copy link
Contributor

esprehn commented Mar 26, 2018

The android CI failure is unrelated:

ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNI.cpp:9:25: fatal error: yoga/YGNode.h: No such file or directory
 #include <yoga/YGNode.h>
                         ^
compilation terminated.
BUILT 294/332 JOBS 0.0s //ReactAndroid/src/test/java/org/mockito/configuration:configuration#class-abi
Build failed: Command failed with exit code 1.
stderr: ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNI.cpp:9:25: fatal error: yoga/YGNode.h: No such file or directory
 #include <yoga/YGNode.h>
                         ^
compilation terminated.
    When running <c++ preprocess_and_compile>.
    When building rule //ReactAndroid/src/main/jni/first-party/yogajni:jni#compile-pic-YGJNI.cpp.o8c77d5e7,default.

@esprehn
Copy link
Contributor

esprehn commented Mar 28, 2018

fwiw this change makes Android match iOS behavior too:

static dispatch_queue_t RCTGetMethodQueue() 
{
 // ...
 queue = dispatch_queue_create("com.facebook.react.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
 // ...
}

iOS uses a separate serial queue to execute AsyncStorage tasks. Android using the default serial AsyncTask queue means all other serial tasks can block AsyncStorage tasks on Android unlike iOS.

@danilobuerger
Copy link
Contributor

Just tested this, works for me.

@dannycochran
Copy link
Contributor Author

@grabbou could you help me find a reviewer? This fixes #14101, which is the 8th most commented open issue right now.

@grabbou
Copy link
Contributor

grabbou commented Apr 9, 2018

@javache @janicduplessis to start with.

@hramos do you know anyone at FB right now involved in Android part of things?

@sunnylqm

This comment has been minimized.

@facebook-github-bot
Copy link
Contributor

@dannycochran I tried to find reviewers for this pull request and wanted to ping them to take another look. However, based on the blame information for the files in this pull request I couldn't find any reviewers. This sometimes happens when the files in the pull request are new or don't exist on master anymore. Is this pull request still relevant? If yes could you please rebase? In case you know who has context on this code feel free to mention them in a comment (one person is fine). Thanks for reading and hope you will continue contributing to the project.

@esprehn
Copy link
Contributor

esprehn commented Apr 23, 2018

Not sure what the bot is talking about. The file dates to the original release of RN in 2015, and the file still exists:
https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/storage/AsyncStorageModule.java

@dannycochran
Copy link
Contributor Author

@facebook-github-bot @janicduplessis requesting a review here. This is still a relevant issue affecting pretty much every Android developer. It is now the 6th most commented open issue, up two spots from three weeks ago :)

@janicduplessis
Copy link
Contributor

Sorry about the delay reviewing this.

Would it be possible to use newSingleThreadExecutor https://developer.android.com/reference/java/util/concurrent/Executors.html#newSingleThreadExecutor() instead of copying that code from AsyncTask.

cc @mdvacca

@esprehn
Copy link
Contributor

esprehn commented May 3, 2018

@janicduplessis That would create a new thread instead of reusing one from the pool that backs AsyncTask, but yeah you could do that.

@silasrm
Copy link

silasrm commented May 10, 2018

My app has this error when try to setItem in wrong format, like JSON without stringify.

@crobinson42
Copy link

@dannycochran would love to see this moved along as the issue is now locked and it's a growing problem for folks.

@fshafiee
Copy link

fshafiee commented Jun 9, 2018

The bugs which this PR resolves, makes it really diffcult to debug apps on Android. The changes seem small and incremental. Wondering what's the hold up for the reviewers...

@rikur

This comment has been minimized.

@esprehn
Copy link
Contributor

esprehn commented Jun 21, 2018

@gaearon This is another critical issue that impacts us. We had to fork the entire AsyncStorage module into our own codebase so we could apply this patch and set canOverrideExistingModule() to true. This PR has been open now for for 3 months. It'd be really awesome if you could help move this along, you had mentioned folks should ping you if PRs and issues were not being addressed.

hramos pushed a commit to hramos/react-native that referenced this pull request Jun 28, 2018
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 facebook#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#16905

[Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely.
Closes facebook#18522

Differential Revision: D8624088

fbshipit-source-id: 2a599fbca270a4826063cd93be8eaf796f34f57a
@hramos
Copy link
Contributor

hramos commented Jun 28, 2018

I was able to reproduce on Circle CI the same failures we observed internally. See https://circleci.com/gh/hramos/react-native/3233?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link, an example:

FAILURE com.facebook.react.modules.storage.AsyncStorageModuleTest testMultiRemove: 
Wanted but not invoked:
callback.invoke();
-> at com.facebook.react.modules.storage.AsyncStorageModuleTest.testMultiRemove(AsyncStorageModuleTest.java:146)
Actually, there were zero interactions with this mock.

Wanted but not invoked:
callback.invoke();
-> at com.facebook.react.modules.storage.AsyncStorageModuleTest.testMultiRemove(AsyncStorageModuleTest.java:146)
Actually, there were zero interactions with this mock.

	at com.facebook.react.modules.storage.AsyncStorageModuleTest.testMultiRemove(AsyncStorageModuleTest.java:146)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.powermock.modules.junit4.rule.PowerMockStatement$1.run(PowerMockRule.java:52)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.powermock.reflect.internal.WhiteboxImpl.performMethodInvocation(WhiteboxImpl.java:1873)
	at org.powermock.reflect.internal.WhiteboxImpl.doInvokeMethod(WhiteboxImpl.java:773)
	at org.powermock.reflect.internal.WhiteboxImpl.invokeMethod(WhiteboxImpl.java:638)
	at org.powermock.reflect.Whitebox.invokeMethod(Whitebox.java:401)
	at org.powermock.classloading.ClassloaderExecutor.execute(ClassloaderExecutor.java:98)
	at org.powermock.classloading.ClassloaderExecutor.execute(ClassloaderExecutor.java:78)
	at org.powermock.modules.junit4.rule.PowerMockStatement.evaluate(PowerMockRule.java:49)
	at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
	at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
	at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runners.Suite.runChild(Suite.java:128)
	at org.junit.runners.Suite.runChild(Suite.java:27)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
	at com.facebook.buck.testrunner.JUnitRunner.run(JUnitRunner.java:97)
	at com.facebook.buck.testrunner.BaseRunner.runAndExit(BaseRunner.java:272)
	at com.facebook.buck.testrunner.JUnitMain.main(JUnitMain.java:48)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.facebook.buck.jvm.java.runner.FileClassPathRunner.main(FileClassPathRunner.java:80)

Copy link
Contributor

@hramos hramos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_android is failing.

@hramos hramos removed Import Failed Import Started This pull request has been imported. This does not imply the PR has been approved. labels Jun 28, 2018
@mcavaliere
Copy link

mcavaliere commented Jul 2, 2018

The Java patch unfortunately didn't help me.

For those that are stuck, I wrote this as a workaround. Mapping getItem() / setItem() / removeItem() to an array of Promises got the job done for me.

Gist: https://gist.githubusercontent.com/mcavaliere/5ff291c7d30198c7e174ac7fd233794c/raw/c4b05821e967ba005b992f3b764ca6c6b23e1c19/AsyncStorageSupplement.js

Code:

export default class AsyncStorageSupplement {
  static multiGet(keys) {
    return Promise.all(
      keys.map(key => AsyncStorage.getItem(key))
    )
  }

  static multiRemove(keys) {
    return Promise.all(
      keys.map(key => AsyncStorage.removeItem(key))
    )
  }

  static multiSet(pairs) {
    return Promise.all(
      pairs.map(pair => AsyncStorage.setItem(pair[0], pair[1]))
    )
  }
}

@adaltavo
Copy link

@mcavaliere your workaround worked for me. Is the problem releted only to "multi" prefixed methods???

@haggholm
Copy link

@hramos Looking at the CI failure (https://circleci.com/gh/facebook/react-native/38275?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link), the error looks completely unrelated to this PR:

ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNI.cpp:9:25: fatal error: yoga/YGNode.h: No such file or directory
 #include <yoga/YGNode.h>
                         ^
compilation terminated.

But the PR does not touch any of the files involved—it just changes one Java file. I would have to guess that the failure was due to something else breaking the build, and if master passes, the PR should too. Could you maybe re-run CI and see if it passes test_android? It would be extremely nice to have this in the next version of RN and not have to patch it anymore…

@hramos
Copy link
Contributor

hramos commented Jul 17, 2018

@haggholm test_android is passing on master, so as long as it's red on this PR, it won't be merged. Perhaps all that's missing is a rebase, and that can be done prior to merging this.

@haggholm
Copy link

@hramos I have to admit I’m kind of crap with git and not familiar with rebase and such, but (a) it looks like the OP has not been active in a while, and (b) I do really want to see this make it in. In case it helps, I created a mirror PR for CI: #20258

Anything I can do to help get this change in, please let me know.

@haggholm
Copy link

@hramos I had to make some changes to make tests pass; once the earlier (irrelevant) errors were out of the way, the AsyncStorageModule tests did actually fail due to async issues; after some minor changes, my mirror PR (#20258) successfully passes test_android. Some other tests are failing, but right now those are also failing in master, and I hardly think that my change to two Java files can be blamed for iOS failures…

@hramos
Copy link
Contributor

hramos commented Jul 20, 2018

Thanks @haggholm, I'll give @dannycochran a chance to take in your changes from #20258 and see if we can get this merged.

@dannycochran
Copy link
Contributor Author

@haggholm @hramos I synced with master and with the changes from #20258

@haggholm
Copy link

@hramos Can this be merged now? It looks like the only failing test is iOS E2E, which can hardly depend on changes to .java files…

@facebook-github-bot facebook-github-bot added the Import Started This pull request has been imported. This does not imply the PR has been approved. label Jul 30, 2018
Copy link
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hramos is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@react-native-bot
Copy link
Collaborator

This pull request was closed by Daniel Cochran in 1b09bd7.

Once this commit is added to a release, you will see the corresponding version tag below the description at 1b09bd7. If the commit has a single master tag, it is not yet part of a release.

@facebook facebook locked as resolved and limited conversation to collaborators Jul 30, 2018
@react-native-bot react-native-bot added the Merged This PR has been merged. label Jul 30, 2018
@dulmandakh
Copy link
Contributor

@dannycochran thank you for investigating and coming up with solution to the common issue. Could you please look at my solution (#20386).

IMHO, using dedicated thread executor to access SQLite will improve performance because AsyncTask.THREAD_POOL_EXECUTOR is shared and might be busy with other AsyncTask tasks. Also it allows you to provide custom executor if you want concurrency etc.

darabi pushed a commit to darabi/react-native that referenced this pull request Aug 9, 2018
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 facebook#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#16905

[Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely.
Pull Request resolved: facebook#18522

Differential Revision: D8624088

Pulled By: hramos

fbshipit-source-id: a1d2e3458d98467845cb34ac73f2aafaaa15ace2
darabi pushed a commit to darabi/react-native that referenced this pull request Aug 10, 2018
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 facebook#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#16905

[Android] [BUGFIX] [AsyncStorage] - Fix AsyncStorage causing remote debugger to hang indefinitely.
Pull Request resolved: facebook#18522

Differential Revision: D8624088

Pulled By: hramos

fbshipit-source-id: a1d2e3458d98467845cb34ac73f2aafaaa15ace2
@hramos hramos removed Import Started This pull request has been imported. This does not imply the PR has been approved. labels Feb 6, 2019
Corey-Peyton added a commit to Corey-Peyton/async-storage that referenced this pull request Jul 14, 2021
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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Platform: Android Android applications.
Projects
None yet
Development

Successfully merging this pull request may close these issues.