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

Fix non-thread-safety of TrySetResultAsync #2349

Closed
wants to merge 3 commits into from

Conversation

shkdee
Copy link

@shkdee shkdee commented Apr 9, 2018

The extension method TaskExtensions.TrySetResultAsync has non-deterministic results when called twice on the same instance, even on the same thread: it creates one task for each call, which may not run in their creation order.
This PR adds a wrapper class around TaskCompletionSource (AsyncTaskCompletionSource), that takes care of calling TrySetResult in an async fashion while ensuring that only the first call is executed. I would really recommend using it instead of TaskCompletionSource each time TaskExtensions.TrySetResultAsync is used (even if it is called only once in any logical workflow), because this non-determinism can cause very subtle and hard-to-track bugs.

This PR also updates the test case that could be violated with the previous behavior (but that was hard to trigger with only one test).

shkdee added 2 commits April 9, 2018 13:02
The extension method TrySetResultAsync has non-deterministic results when called twice on the same instance, even on the same thread: it creates one task for each call, which may not run in their creation order.
This commit adds a wrapper around TaskCompletionSource, that ensures that only the first call to TrySetResultAsync is really executed.
It also updates the test case that could be violated with the previous behavior (but that was hard to trigger with only one test).
fix code style
@AppVeyorBot
Copy link

@amaitland
Copy link
Member

Ideally we'd update to .Net 4.6 and remove TrySetResultAsync.

Other than the single test case what real world use case are you experiencing an issue with? Which CEF callback is calling TrySetResultAsync twice?

@@ -89,8 +89,14 @@ public static TaskCompletionSource<TResult> WithTimeout<TResult>(this TaskComple
}

/// <summary>
/// <para>
Copy link
Member

Choose a reason for hiding this comment

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

If we go with your proposal, can this method be deleted?

Copy link
Author

Choose a reason for hiding this comment

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

I guess it could, it's just still used in a few places in the examples.

Copy link
Member

Choose a reason for hiding this comment

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

We can look at upgrading the example projects to .Net 4.6 if required.

Only the core projects need to remain as .Net 4.5.2

Copy link
Author

Choose a reason for hiding this comment

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

I'll let you do that then.
I also just noticed it was used in two places in the CefSharp.Core project, and I'm much less familiar with C++/CLI to handle that case.

/// <param name="result">result</param>
public void TrySetResultAsync(T result)
{
if (Interlocked.Exchange(ref resultSet, 1) == 0)
Copy link
Member

Choose a reason for hiding this comment

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

Code consistently, braces required even for single line if block.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

/// continuations must not run on CEF UI threads.
/// </summary>
/// <typeparam name="T"></typeparam>
public class AsyncTaskCompletionSource<T>
Copy link
Member

Choose a reason for hiding this comment

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

This can probably be made internal.

Copy link
Author

@shkdee shkdee Apr 9, 2018

Choose a reason for hiding this comment

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

It's also used in the project CefSharp.OffScreen (once), so it cannot actually.

Copy link
Member

Choose a reason for hiding this comment

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

Last I checked IntetnalsVisibleTo was working for all the c# projects, just doesn't work with the VC++ ones, so we do have to leave it public if it's going to be used in C++.

https://github.com/cefsharp/CefSharp/blob/master/CefSharp/Properties/AssemblyInfo.cs#L26

Copy link
Author

Choose a reason for hiding this comment

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

Oh sorry, I missed the InternalsVisibleTo. Well then it's ok to make it internal except if we also want to replace the two usages of TaskExtensions::TrySetResultAsync which are in the C++ project.

Copy link
Member

Choose a reason for hiding this comment

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

Ok no worries, will have to remain public.

Copy link
Contributor

Choose a reason for hiding this comment

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

guys why dont u use the default TaskCompletionSource and create it with TaskCreationOptions.RunContinuationsAsync ?

Copy link
Member

Choose a reason for hiding this comment

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

guys why dont u use the default TaskCompletionSource and create it with TaskCreationOptions.RunContinuationsAsync ?

TaskCreationOptions.RunContinuationsAsync was added in .Net 4.6

We are still supporting .Net 4.5.2 (no plans to change this)

One day I'll get around to updating the .Net Core builds to use RunContinuationsAsync

public Task<T> Task => tcs.Task;

/// <summary>
/// Set the TaskCompletionSource in an async fashion. This prevents the Task Continuation being executed
Copy link
Member

Choose a reason for hiding this comment

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

This needs rewording. I had to read it three times, your new sentence about sync on same thread could be clearer. Should probably also link to a relevant resource, I should have done that previously.

Copy link
Author

Choose a reason for hiding this comment

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

By "a relevant resource", you mean regarding executing code on CEF UI threads ? So something like the general usage guide of the wiki ?

@shkdee
Copy link
Author

shkdee commented Apr 9, 2018

Well as you wish, that would be a good solution too.

I found that bug when using AsyncExtensions.SetCookieAsync(this ICookieManager cookieManager, string url, Cookie cookie); that would sometimes randomly return false, due to this bug (it uses TaskSetCookieCallback internally, which is one of the callback implementations modified by this PR). I did not try to trigger the bug for the other ones but in any case, the method TaskExtensions.TrySetResultAsync is flawed.

@AppVeyorBot
Copy link

@amaitland
Copy link
Member

Upgrading to .Net 4.6 currently isn't an option for the core projects.

It looks like there are only a couple of classes that have the potential for this bug. Looks like only the callbacks have the potential to make multiple calls. I guess the question is should we limit the scope of this PR to only the relevant classes?

Will have a think about it, comments welcome.

@shkdee
Copy link
Author

shkdee commented Apr 9, 2018

For that PR only, it would be easier and much less breaking to limit it to the relevant classes.

But in the long term, for the CefSharp project, my opinion on the matter is that the current method TaskExtensions.TrySetResultAsync is a bit dangerous: it does not behave like the .Net framework method TaskCompletionSource<>.TrySetResult it intends to replace (because of this bug), so it's misleading and error-prone (it's really easy to call it twice if you are not very careful, because that would not bother you for the origin method). As such, it should be removed/obsoleted as much as possible.

@amaitland
Copy link
Member

TrySetResultAsync is only intended to be used within the context of CefSharp it's self, so the scope for error in usage is very small. I think upgrading the example projects to .Net 4.6 and removing is usage from those examples makes a lot of sense, we shouldn't be encouraging others to use the same method.

If TrySetResultAsync does remain, then it should be renamed for clarity, it's sole purpose is to ensure the continuation happens on the thread pool and not on the CEF UI thread.

@amaitland amaitland closed this in 43c1ff3 Apr 30, 2018
@amaitland amaitland reopened this Apr 30, 2018
amaitland added a commit that referenced this pull request Apr 30, 2018
…r called

Add IsDisposed property to ICompletionCallback, IPrintToPdfCallback, IRegisterCdmCallback, IResolveCallback
Should resolve #2349 until the code review can be finished
@amaitland
Copy link
Member

I feel this needs more work that I have time for just at the moment, to resolve the issue with the callbacks I've applied a very simple bool check, see commit 43c1ff3 for details.

The complete and dispose methods should always be called on the same thread (dispose is slightly different to the standard .net dispose in this context as it's being called from unmanged code when the parent ref pointer goes out of scope). This should resolve the short term issue in the next release and I'll come back to this when I have some time.

@amaitland
Copy link
Member

I'll resolve the merge conflicts when it comes time to merge this.

amaitland added a commit that referenced this pull request May 9, 2018
amaitland added a commit that referenced this pull request May 13, 2018
… a long term solution for #2349 is devised

- Add OnComplete check to all the TaskCompletionSource based callbacks that could potentially have executed TrySetResultAsync twice, which could on rare occasions
execute in a non deterministic fashion (The Disposed execution of TrySetResultAsync called before the OnComplete one). See #2349 for a more detailed
proposal and one potential long term solution (upgrading to .Net 4.6 is the best option, though not practical now, will update the example projects shortly).
- Update callbacks for consistency, all now implement IDisposable and have an IsDisposed
@amaitland
Copy link
Member

#4482 updates to .Net 4.6.2 and removes TrySetResultAsync.

@amaitland amaitland closed this Apr 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants