Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

[release/2.2] Fix CancellationTokenRegistration.Token after CTS.Dispose #21417

Merged
merged 1 commit into from
Dec 7, 2018

Conversation

stephentoub
Copy link
Member

CTR.Token should never throw, but it's currently throwing an ObjectDisposedException if the associated CancellationTokenSource has been disposed.

Ports #21394 to release/2.2
Fixes https://github.com/dotnet/corefx/issues/33844

CTR.Token should never throw, but it's currently throwing an ObjectDisposedException if the associated CancellationTokenSource has been disposed.
@stephentoub stephentoub changed the title Fix CancellationTokenRegistration.Token after CTS.Dispose [release/2.2] Fix CancellationTokenRegistration.Token after CTS.Dispose Dec 6, 2018
@stephentoub
Copy link
Member Author

stephentoub commented Dec 6, 2018

Description

We added the CancellationTokenRegistration.Token property in .NET Core 2.1. Since the CTR already knows with what token it's associated, allowing you to get the token from the CTR saves you from also needing to separately store the token, which lets you improve memory utilization by not carrying it around effectively twice. We took advantage of that, for example, in FileStream's Read/WriteAsync operations, to decrease the size of the object allocated for those operations. However, the CTR.Token property is incorrectly throwing an ObjectDisposedException if the associated CancellationTokenSource has been disposed, which introduces an ODE where there wasn't previously one (just accessing a CancellationToken you already had stored). This is causing regressions in code that created a token source, registered with its token, and then disposed the source.

Customer Impact

e.g.
Exceptions on background threads that crash the process when performing operations like:

var cts = new CancellationTokenSource();
Task<int> t = fileStream.ReadAsync(..., cts.Token);
cts.Dispose();
int result = await t;

ASP.NET actually has a case of this today, e.g. dotnet/aspnetcore#4447.

Regression?

Yes. It's not a regression in CancellationTokenRegistration.Token, which was only introduced in 2.1, but rather a regression in that it's behavior isn't matching code it was used to replace, so it's for example a regression in FileStream.Read/WriteAsync.

Packaging reviewed?

Change affects System.Private.CoreLib only.

Risk

Minimal. The new code is effectively the same as the old, except avoiding a ThrowIfDisposed call.

@RussKeldorph RussKeldorph added the Servicing-consider Issue for next servicing release review label Dec 6, 2018
@RussKeldorph RussKeldorph added this to the 2.2.x milestone Dec 6, 2018
@vivmishra vivmishra added Servicing-approved Approved for servicing release and removed Servicing-consider Issue for next servicing release review labels Dec 6, 2018
@vivmishra
Copy link

Approved for 2.2.1.

  • ASP.NET should validate if this fixes their issue.

We should wait for any 2.1 fix -- close the issue.

@muratg
Copy link

muratg commented Dec 6, 2018

cc @halter73 @Tratcher

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

Looks reasonable.

@muratg
Copy link

muratg commented Dec 6, 2018

@halter73 Let's verify that this fixes the customer issue (once we have a build.) Thanks.

@stephentoub
Copy link
Member Author

@halter73, can you help validate it fixes the ASP.NET issue?

@halter73
Copy link
Member

halter73 commented Dec 6, 2018

@stephentoub Sure. Are there any build artifacts I can use to test this PR or do I have to build myself?

@stephentoub
Copy link
Member Author

@halter73
Copy link
Member

halter73 commented Dec 7, 2018

I used the "checked" x64 artifacts from https://ci.dot.net/job/dotnet_coreclr/job/release_2.2/job/x64_checked_windows_nt_innerloop_prtest/231/artifact/bin/Product/Windows_NT.x64.Checked/.

This was easier for me since I already have the x64 SDKs installed on my workstation. I simply dropped those artifacts into $DOTNET_HOME\shared\Microsoft.NETCore.App\2.2.0\ and tested against that.

My repro app which consistently crashes with an ODE in less than a minute on the 2.2.0 runtime, didn't crash at all in 10 minutes of testing when the runtime was patched with the artifacts from this PR.

@stephentoub Is there a better way of using these artifacts for testing? If you want to try the repro yourself, it's super simple: just clone git@github.com:halter73/anc-4422.git and dotnet run.

C:\dev\halter73\anc-4422 [master ≡ +2 ~0 -0 !]> dotnet run
Hosting environment: Development
Content root path: C:\dev\halter73\anc-4422
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Unhandled Exception:
Unhandled Exception:
Unhandled Exception: System.ObjectDisposedException: The CancellationTokenSource has been disposed.
   at System.Threading.CancellationTokenSource.ThrowObjectDisposedException()
   at System.IO.FileStream.FileStreamCompletionSource.CompleteCallback(UInt64 packedResult)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
System.ObjectDisposedException: The CancellationTokenSource has been disposed.
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)   at System.Threading.CancellationTokenSource.ThrowObjectDisposedException()
System.ObjectDisposedException: The CancellationTokenSource has been disposed.
   at System.IO.FileStream.FileStreamCompletionSource.CompleteCallback(UInt64 packedResult)
   at System.Threading.CancellationTokenSource.ThrowObjectDisposedException()
   at System.IO.FileStream.FileStreamCompletionSource.CompleteCallback(UInt64 packedResult)

   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)

--- End of stack trace from previous location where exception was thrown ---
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
...

@stephentoub
Copy link
Member Author

Very helpful, @halter73, thank you. I generally do something similar, patching my shared framework. I did the same here, using a locally-built release build, and no longer see the crashes that I generally saw with your repro very quickly, often within 10 seconds.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Threading Servicing-approved Approved for servicing release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants