Skip to content
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using NUnit.Framework;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ReactNative.Modules.Storage;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -37,14 +38,18 @@ public void Init()
}

[TearDown]
public void Cleanup()
public void TearDown()
{
module.clear(callback);
Assert.That(waitHandle.WaitOne(), Is.True);
Assert.That(error, Is.Null);
Assert.That(result, Is.Null);
module.clear(new MockCallback((invoke) => { }));
}

[Test]
public void AsyncStorageModule_DoesNotInvokeCallbackAfterDispose()
{
callback = new MockCallback(invoke => throw new Exception("should not be called"));
Copy link
Contributor

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.

module.OnReactInstanceDispose();
module.multiGet(new[] { "" }, callback);
}

[Test]
public void AsyncStorageModule_InvalidKeyValue_Method()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand All @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
I tried prototyping a race condition and looks like not cancelling the wait results in a the thread waiting there for ever.

try
{
Expand All @@ -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);
Expand Down Expand Up @@ -274,6 +289,8 @@ public async void getAllKeys(ICallback callback)

public override void OnReactInstanceDispose()
{
_cancelSource.Cancel();
Copy link
Contributor

Choose a reason for hiding this comment

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

If you use CancellationDisposable from System.Reactive, it only requires a Dispose call, and I think it will protect you when accessing the token (so you won't need the ObjectDisposedException catch block below).

Copy link
Contributor

@psivaram psivaram Sep 20, 2018

Choose a reason for hiding this comment

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

Should we be taking the mutex here too?
What happens if a read/write is in progress where the mutex was taken.
We dispose the mutex and then the read/write completes and tries to release the mutex (which has now been disposed)

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();
}

Expand Down Expand Up @@ -358,5 +375,32 @@ private async Task<IFolder> GetAsyncStorageFolder(bool createIfNotExists)

return null;
}

private async Task<bool> IsCancelledOrDisposed()
{
try
{
var token = _cancelSource.Token;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use the mutex and check the IsCanceled property rather than relying on exception behavior? I know we'll likely need the catch blocks anyway, but it might prevent a few frequent exceptions.

await _mutex.WaitAsync(token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

can we add a when clause to this so we only catch when the OperationCanceledException is sourced from the correct cancellation token?

// 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;
}
}
}