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

[Windows] Unwrap AggregateException when catching exceptions from Task.Wait() method #602

Merged
merged 9 commits into from
Apr 16, 2018

Conversation

snrnats
Copy link
Contributor

@snrnats snrnats commented Feb 19, 2018

We need to unwrap AggregateException when using Task.Wait() method. So that the caller will get original exception.

@msftclas
Copy link

msftclas commented Feb 19, 2018

CLA assistant check
All CLA requirements met.

@guperrot
Copy link
Member

Hi, we are not sure what you are trying to improve here.
Can you tell us more about the intent of this change?

@snrnats
Copy link
Contributor Author

snrnats commented Feb 20, 2018

In all cases Wait method is called on tasks that can throw StorageException. But Wait call will wrap up this exception with AggregateException according to docs .

Here is the issue: the caller of PutLog method expects StorageException type of exception and tries to catch it (see Changel.PersistLogAsync()), but it won’t happen because AggregateException will be thrown and no code will handle it.

I changed all other Wait calls for consistency and to prevent further issues.

@guperrot
Copy link
Member

Hi, I mean, what does this change achieve for real? How is user experience impacted and what are the motivations for this?

@snrnats
Copy link
Contributor Author

snrnats commented Feb 20, 2018

My app is affected by the issues I described. I've got some crash reports when using AppCenter.Push 1.3.

Application Specific Information:
TaskExceptionHolder_UnhandledException (AggregateException_ctor_DefaultMessage (The storage operation failed))

Exception Stack:
unknown location
SharedLibrary!<BaseAddress>+0x4eb21d
System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, Threading.CancellationToken cancellationToken)
System.Threading.Tasks.Task.Wait()
Microsoft.AppCenter.Storage.Storage.<>c__DisplayClass12_0.<PutLog>b__0()
System.Action.Invoke()
SharedLibrary!<BaseAddress>+0x4f52e5
SharedLibrary!<BaseAddress>+0x4f520c
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
...
SQLite.SQLiteException: Busy
Microsoft.AppCenter.Storage.StorageException: The storage operation failed
System.AggregateException: AggregateException_ctor_DefaultMessage (The storage operation failed)
System.AggregateException: TaskExceptionHolder_UnhandledException (AggregateException_ctor_DefaultMessage (The storage operation failed))

@guperrot
Copy link
Member

Do you have a reproduction scenario for this crash?

@snrnats
Copy link
Contributor Author

snrnats commented Feb 20, 2018

I don't have steps. It reproduces not very often according to crash count. For now I can tell that the crash occurs only while executing code from in process background task.

@guperrot
Copy link
Member

guperrot commented Feb 20, 2018

But how do we know this patch solves a crash that we don't know how to reproduce?

@snrnats
Copy link
Contributor Author

snrnats commented Feb 21, 2018

My crash logs say that app crashes because AggregateException isn't handled in the code. In our case the AggregateException contains StorageException. Try-catch for StorageException is already presented in
Changel.PersistLogAsync(). We just need to unwrap the exception.

It will prevent the crash. Although I am not sure about the reason of underlying SQLite.SQLiteException: Busy

Copy link
Member

@guperrot guperrot left a comment

Choose a reason for hiding this comment

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

Please try adding a unit test that (with mocks) that would crash if we don't have this patch.

@@ -52,7 +53,7 @@ public Storage() : this(DefaultAdapter())
internal Storage(IStorageAdapter adapter)
{
_storageAdapter = adapter;
_queue.Add(new Task(() => InitializeDatabaseAsync().Wait()));
Copy link
Member

Choose a reason for hiding this comment

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

Like @MatkovIvan mentioned in a conversation we had, just using .GetAwaiter().GetResult() would remove the need for an extension utils, please switch to that and remove the additional extension.

@snrnats
Copy link
Contributor Author

snrnats commented Feb 21, 2018

Hi @guperrot
I will try to add unit test a little bit later.

@@ -85,7 +85,7 @@ public void GetLogsQueryError()
var mockAdapter = new Mock<IStorageAdapter>();
mockAdapter.Setup(
a => a.GetAsync(It.IsAny<PredType>(), It.IsAny<int>()))
.Throws(new StorageException());
.ThrowsAsync(new StorageException());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ThrowsAsync() allows to setup mock that returns faulted task. Throws() throws right away without returning a task.

@@ -8,26 +8,11 @@ public static class TaskExtension
{
Copy link
Member

Choose a reason for hiding this comment

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

Hi, thanks for the changes. However since we use GetAwaiter().GetResult() directly in code, we should remove this utility.

@snrnats
Copy link
Contributor Author

snrnats commented Feb 23, 2018

Hi. I noticed strange behavior of unit tests. Running all tests complete successfully, but if I filter tests with "Storage" and run them then ThrowStorageExceptionInDeleteLogsTime will fail.

Debug Trace:
[AppCenter] WARN: Channel is temporarily disabled; log was saved to disk
[AppCenter] DEBUG: CheckPendingLogs(channelName) pending log count: 1
[AppCenter] DEBUG: triggerIngestion(channelName) pendingLogCount=1
[AppCenter] DEBUG: CheckPendingLogs(channelName) pending log count: 0
[AppCenter] WARN: Could not delete logs for batch test-batch-id
Microsoft.AppCenter.Storage.StorageException: The storage operation failed
   at Moq.MethodCall.Execute(ICallContext call)
   at Moq.MethodCallReturn`2.Execute(ICallContext call)
   at Moq.ExecuteCall.HandleIntercept(ICallContext invocation, InterceptorContext ctx, CurrentInterceptContext localctx)
   at Moq.Interceptor.Intercept(ICallContext invocation)
   at Moq.Proxy.CastleProxyFactory.Interceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.Proxies.ObjectProxy_1.DeleteLogs(String channelName, String batchId)
   at Microsoft.AppCenter.Channel.Channel.HandleSendingSuccess(State state, String batchId) in D:\Work\AppCenter-SDK-DotNet\SDK\AppCenter\Microsoft.AppCenter.Windows.Shared\Channel\Channel.cs:line 432

Seems like 'HandleSendingSuccess' needs to await for _storage.DeleteLogs(Name, batchId), but even then the test will fail. Do you observe the same behavior?

devenv_2018-02-23_10-43-04

@guperrot guperrot dismissed their stale review February 23, 2018 20:28

reset feedback

Copy link
Member

@guperrot guperrot left a comment

Choose a reason for hiding this comment

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

On develop if I run those 4 tests at once like you did, they are all green.
However on your branch it fails so this is a regression that needs to be fixed.

@guperrot guperrot added the bug label Feb 27, 2018
@snrnats
Copy link
Contributor Author

snrnats commented Mar 1, 2018

Waiting for a next release of moq4 where an issue affecting this PR will be resolved

@guperrot
Copy link
Member

guperrot commented Apr 3, 2018

Hi @snrnats, are you still blocked on moq release?

@snrnats
Copy link
Contributor Author

snrnats commented Apr 3, 2018

Hi @guperrot . Yes, still waiting for the next moq4 release.

I'm trying to reach out to authors of the moq4, hope they will give us more information about the next release

Update: probable there will be a release of moq4 in the next few days

@guperrot guperrot changed the title Unwrap AggregateException when catching exceptions from Task.Wait() method [Windows] Unwrap AggregateException when catching exceptions from Task.Wait() method Apr 10, 2018
@guperrot
Copy link
Member

You will also need to fix the conflicts, a big refactoring was introduced to address #517.

@@ -85,11 +85,11 @@ public void GetLogsQueryError()
var mockAdapter = new Mock<IStorageAdapter>();
mockAdapter.Setup(
a => a.GetAsync(It.IsAny<PredType>(), It.IsAny<int>()))
.Throws(new StorageException());
.ThrowsAsync(new StorageException());
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use

.Throws(TaskExtension.GetFaultedTask(new StorageException()))

to avoid moq bug.

Copy link
Contributor Author

@snrnats snrnats Apr 14, 2018

Choose a reason for hiding this comment

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

This line of code produce an error Error CS1503 Argument 1: cannot convert from 'System.Threading.Tasks.Task<Microsoft.AppCenter.Storage.StorageException>' to 'System.Exception'.

Anyway Throws() is implemented differently then ThrowsAsync(). I think that in most cases we should use ThrowsAsync(). Compare

        /// Throws exception
        private static Task Throw()
        {
            throw new Exception();
            return Task.Delay(1000);
        }

        /// Return faulted task
        private static async Task ThrowAsync()
        {
            throw new Exception();
            await Task.Delay(1000);
        }

I have another way to rewrite it. We could use Returns() with a faulted task. It will look bulky but will do what was intended.
.Returns(TaskExtension.GetFaultedTask<List<Microsoft.AppCenter.Storage.Storage.LogEntry>>(new StorageException()))

I can try to change it in a such way if we don't want to wait for a moq4 release.

Copy link
Contributor

@MatkovIvan MatkovIvan Apr 14, 2018

Choose a reason for hiding this comment

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

Oops, I mean Returns with a faulted task of course. You understood my idea correctly 👍

@@ -24,7 +24,7 @@ public void InitializeStorageTest()
[TestMethod]
public void CountEmptyStorage()
{
var count = _storage.CountLogsAsync(StorageTestChannelName).RunNotAsync();
var count = _storage.CountLogsAsync(StorageTestChannelName).GetAwaiter().GetResult();
Copy link
Contributor

Choose a reason for hiding this comment

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

Tests can be async and I've already replaced it partially.
async/await looks like right replacement instead of GetAwaiter().GetResult()
You don't necessarily change all the tests in the project, only those that relate to your fix.

@snrnats
Copy link
Contributor Author

snrnats commented Apr 14, 2018

I reverted changes that are not related to fix

@guperrot guperrot merged commit 478d442 into microsoft:develop Apr 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants