-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fix/workaround a crash when ReactInstance is disposed while AsyncStorage still doing work #1888
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
Changes from all commits
2ac17e2
32e0716
fae3de9
5cdd4b4
8e86d2f
9b110ea
635f524
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| // Copyright (c) 2015-present, Facebook, Inc. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using Newtonsoft.Json; | ||
| using Newtonsoft.Json.Linq; | ||
| using PCLStorage; | ||
|
|
@@ -15,6 +16,8 @@ namespace ReactNative.Modules.Storage | |
| class AsyncStorageModule : NativeModuleBase | ||
| { | ||
| private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1, 1); | ||
| private CancellationTokenSource _cancelSource = new CancellationTokenSource(); | ||
|
|
||
|
|
||
| public override string Name | ||
| { | ||
|
|
@@ -27,6 +30,8 @@ public override string Name | |
| [ReactMethod] | ||
| public async void multiGet(string[] keys, ICallback callback) | ||
| { | ||
| if (await IsCancelledOrDisposed()) return; | ||
|
|
||
| if (keys == null) | ||
| { | ||
| callback.Invoke(AsyncStorageHelpers.GetInvalidKeyError(null), null); | ||
|
|
@@ -69,6 +74,8 @@ public async void multiGet(string[] keys, ICallback callback) | |
| [ReactMethod] | ||
| public async void multiSet(string[][] keyValueArray, ICallback callback) | ||
| { | ||
| if (await IsCancelledOrDisposed()) return; | ||
|
|
||
| if (keyValueArray == null || keyValueArray.Length == 0) | ||
| { | ||
| callback.Invoke(AsyncStorageHelpers.GetInvalidKeyError(null)); | ||
|
|
@@ -125,6 +132,8 @@ public async void multiSet(string[][] keyValueArray, ICallback callback) | |
| [ReactMethod] | ||
| public async void multiRemove(string[] keys, ICallback callback) | ||
| { | ||
| if (await IsCancelledOrDisposed()) return; | ||
|
|
||
| if (keys == null || keys.Length == 0) | ||
| { | ||
| callback.Invoke(AsyncStorageHelpers.GetInvalidKeyError(null)); | ||
|
|
@@ -169,6 +178,8 @@ public async void multiRemove(string[] keys, ICallback callback) | |
| [ReactMethod] | ||
| public async void multiMerge(string[][] keyValueArray, ICallback callback) | ||
| { | ||
| if (await IsCancelledOrDisposed()) return; | ||
|
|
||
| if (keyValueArray == null || keyValueArray.Length == 0) | ||
| { | ||
| callback.Invoke(AsyncStorageHelpers.GetInvalidKeyError(null)); | ||
|
|
@@ -225,6 +236,8 @@ public async void multiMerge(string[][] keyValueArray, ICallback callback) | |
| [ReactMethod] | ||
| public async void clear(ICallback callback) | ||
| { | ||
| if (await IsCancelledOrDisposed()) return; | ||
|
|
||
| await _mutex.WaitAsync().ConfigureAwait(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens to these WaitAsync calls which are already in progress? We should pass the cancellation token to them to ensure that they are cancelled before we dispose the mutex. |
||
| try | ||
| { | ||
|
|
@@ -245,6 +258,8 @@ public async void clear(ICallback callback) | |
| [ReactMethod] | ||
| public async void getAllKeys(ICallback callback) | ||
| { | ||
| if (await IsCancelledOrDisposed()) return; | ||
|
|
||
| var keys = new JArray(); | ||
|
|
||
| await _mutex.WaitAsync().ConfigureAwait(false); | ||
|
|
@@ -274,6 +289,8 @@ public async void getAllKeys(ICallback callback) | |
|
|
||
| public override void OnReactInstanceDispose() | ||
| { | ||
| _cancelSource.Cancel(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be taking the mutex here too? Another alternative I can think of is to replace all existing calls to _mutex.Release with calls to a new function SafeReleaseMutex which check IsCancelledOrDisposed before releasing the mutex. |
||
| _cancelSource.Dispose(); | ||
| _mutex.Dispose(); | ||
| } | ||
|
|
||
|
|
@@ -358,5 +375,32 @@ private async Task<IFolder> GetAsyncStorageFolder(bool createIfNotExists) | |
|
|
||
| return null; | ||
| } | ||
|
|
||
| private async Task<bool> IsCancelledOrDisposed() | ||
| { | ||
| try | ||
| { | ||
| var token = _cancelSource.Token; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the mutex and check the |
||
| await _mutex.WaitAsync(token).ConfigureAwait(false); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a |
||
| // in this case OnReactInstanceDispose() was called in the other thread while waiting for the mutex | ||
| // no JS callback invoked intentionally. | ||
| return true; | ||
| } | ||
| catch (ObjectDisposedException) | ||
| { | ||
| // TODO: This is workaround. ReactInstance doesn't stop invoking ReactMethod even after DisposeAsync call. | ||
| // ReactInstance has the responsibility to guarantee flushing of all tasks from JavaScript and NativeModules. | ||
| return true; | ||
| } | ||
| finally | ||
| { | ||
| _mutex.Release(); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make this test a bit more deterministic, can we use a ManualResetEvent and call Wait with a timeout (say 500ms)? Otherwise, if this regresses, the exception that gets thrown could become a red herring / cause another test to fail / cause the test runner to crash.