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

Plumb CancellationToken through Socket.Receive/SendAsync #36516

Merged
merged 1 commit into from
Apr 7, 2019

Conversation

stephentoub
Copy link
Member

In .NET Core 2.1 we added overloads of Send/ReceiveAsync, and we proactively added CancellationToken arguments to them, but those tokens were only checked at the beginning of the call; if a cancellation request came in after that check, the operation would not be interrupted.

This PR plumbs the token through so that a cancellation request at any point in the operation will cancel that operation. On Windows we register to use CancelIoEx to request cancellation of the specific overlapped operation on the specific socket. On Unix we use the existing cancellation infrastructure already in place to support the existing custom queueing scheme.

Some caveats:

  • On Windows, canceling a TCP receive will end up canceling all TCP receives pending on that socket, even when we request cancellation of a specific overlapped operation; this is just how cancellation works at the OS level, and there's little we can do about it. It also shouldn't matter much, as multiple pending receives on the same socket are rare.
  • If multiple concurrent receives or multiple concurrent sends are issued on the same socket, only the first will actually be cancelable. This is because this implementation only plumbs the token through the SocketAsyncEventArgs-based code paths, not the APM based code paths, and currently when using the Task-based APIs, we use the SocketAsyncEventArgs under the covers for only one receive and one send at a time; other receives made while that SAEA receive is in progress or other sends made while that SAEA send is in progress will fall back to using the APM code paths. This could be addressed in the future in various ways, including a) just using the SAEA code paths for all operations and deleting the APM fallback, or b) plumbing cancellation through APM as well. However, for now, this approach addresses the primary use case and should be sufficient.
  • This only affects code paths to which the CancellationToken passed to Send/ReceiveAsync could reach. If in the future we add additional overloads taking CancellationToken, we will likely need to plumb it to more places.

Fixes https://github.com/dotnet/corefx/issues/24430
cc: @geoffkizer, @davidsh, @wfurt, @tmds

@davidsh davidsh added this to the 3.0 milestone Apr 1, 2019
@wfurt
Copy link
Member

wfurt commented Apr 1, 2019

https://mc.dot.net/#/user/dotnet-bot/pr~2Fdotnet~2Fcorefx~2Frefs~2Fpull~2F36516~2Fmerge/test~2Ffunctional~2Fcli~2F/20190331.21/workItem/System.Net.Http.Functional.Tests

GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly test is failing.
It seems like this may be related to this change.

@stephentoub
Copy link
Member Author

GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly test is failing.
It seems like this may be related to this change.

Definitely is. I'll take a look.

In .NET Core 2.1 we added overloads of Send/ReceiveAsync, and we proactively added CancellationToken arguments to them, but those tokens were only checked at the beginning of the call; if a cancellation request came in after that check, the operation would not be interrupted.

This PR plumbs the token through so that a cancellation request at any point in the operation will cancel that operation.  On Windows we register to use CancelIoEx to request cancellation of the specific overlapped operation on the specific socket.  On Unix we use the existing cancellation infrastructure already in place to support the existing custom queueing scheme.

Some caveats:
- On Windows, canceling a TCP receive will end up canceling all TCP receives pending on that socket, even when we request cancellation of a specific overlapped operation; this is just how cancellation works at the OS level, and there's little we can do about it.  It also shouldn't matter much, as multiple pending receives on the same socket are rare.
- If multiple concurrent receives or multiple concurrent sends are issued on the same socket, only the first will actually be cancelable.  This is because this implementation only plumbs the token through the SocketAsyncEventArgs-based code paths, not the APM based code paths, and currently when using the Task-based APIs, we use the SocketAsyncEventArgs under the covers for only one receive and one send at a time; other receives made while that SAEA receive is in progress or other sends made while that SAEA send is in progress will fall back to using the APM code paths.  This could be addressed in the future in various ways, including a) just using the SAEA code paths for all operations and deleting the APM fallback, or b) plumbing cancellation through APM as well.  However, for now, this approach addresses the primary use case and should be sufficient.
- This only affects code paths to which the CancellationToken passed to Send/ReceiveAsync could reach.  If in the future we add additional overloads taking CancellationToken, we will likely need to plumb it to more places.
@stephentoub
Copy link
Member Author

@wfurt, fixed. I needed a try/catch to handle a race condition between the SafeHandle being disposed and trying to use it for cancellation. This showed up in the HTTP tests because we forcefully close the connection as part of cancellation (we could look at revisiting that after this goes in).

@stephentoub
Copy link
Member Author

/azp run corefx-outerloop-windows

@stephentoub
Copy link
Member Author

/azp run corefx-outerloop-linux

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@stephentoub
Copy link
Member Author

/azp run corefx-outerloop-osx

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@stephentoub
Copy link
Member Author

@sebastienros, before merging this, I'd like to make sure this doesn't regress Kestrel benchmarks. What's the best way for me to do that these days?

Copy link
Member

@wfurt wfurt left a comment

Choose a reason for hiding this comment

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

LGTM.
And yes, benchmark would be good.

@tmds
Copy link
Member

tmds commented Apr 4, 2019

@stephentoub the benchmarking still requires some human intervention. For benchmarks @sebastienros is doing with me, I provide him a release build of System.Net.Sockets.dll (baseline version and modified version).

@stephentoub
Copy link
Member Author

stephentoub commented Apr 7, 2019

There doesn't appear to be any meaningful impact on plaintext, positive or negative, which is good (the goal here was to support additional functionality without negatively impacting perf).

The numbers fluctuate a bit from run to run, but here's an example on Windows:

| Description |       RPS | CPU (%) | Memory (MB) | Avg. Latency (ms) | Startup (ms) | First Request (ms) | Latency (ms) | Ratio |
| ----------- | --------- | ------- | ----------- | ----------------- | ------------ | ------------------ | ------------ | ----- |
|    baseline | 2,307,911 |      94 |         158 |              3.94 |          357 |              84.03 |          0.5 |  1.00 |
|   mychanges | 2,330,932 |      90 |         159 |              3.53 |          357 |              84.65 |         0.41 |  1.01 |

and on Linux:

| Description |       RPS | CPU (%) | Memory (MB) | Avg. Latency (ms) | Startup (ms) | First Request (ms) | Latency (ms) | Ratio |
| ----------- | --------- | ------- | ----------- | ----------------- | ------------ | ------------------ | ------------ | ----- |
|    baseline | 2,012,913 |      98 |         176 |              1.32 |          259 |             116.24 |         0.49 |  1.00 |
|   mychanges | 2,008,372 |      98 |         176 |              1.32 |          264 |             118.15 |         0.62 |  1.00 |

@stephentoub stephentoub merged commit 2190a0f into dotnet:master Apr 7, 2019
@stephentoub stephentoub deleted the socketcancellation branch April 7, 2019 20:51
// call TryCancel, so we do this after the op is fully enqueued.
if (cancellationToken.CanBeCanceled)
{
operation.CancellationRegistration = cancellationToken.UnsafeRegister(s => ((TOperation)s).TryCancel(), operation);
Copy link
Member

Choose a reason for hiding this comment

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

@stephentoub a canceled operation will remain in the queue until it is removed at the next event or the queue gets stopped.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. Is that a problem?

Copy link
Member

Choose a reason for hiding this comment

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

Not functionally. It will be alive for longer and maybe keeping some other things alive.

Copy link
Member Author

Choose a reason for hiding this comment

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

It will be alive for longer and maybe keeping some other things alive.

Yes, but then again so are the Socket and the SocketAsyncEventArgs. In comparison this shouldn't keep alive much. And if you're canceling the operation, there's a really high likelihood you're also either tearing everything down or about to do something else that will revisit the queue.

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…fx#36516)

In .NET Core 2.1 we added overloads of Send/ReceiveAsync, and we proactively added CancellationToken arguments to them, but those tokens were only checked at the beginning of the call; if a cancellation request came in after that check, the operation would not be interrupted.

This PR plumbs the token through so that a cancellation request at any point in the operation will cancel that operation.  On Windows we register to use CancelIoEx to request cancellation of the specific overlapped operation on the specific socket.  On Unix we use the existing cancellation infrastructure already in place to support the existing custom queueing scheme.

Some caveats:
- On Windows, canceling a TCP receive will end up canceling all TCP receives pending on that socket, even when we request cancellation of a specific overlapped operation; this is just how cancellation works at the OS level, and there's little we can do about it.  It also shouldn't matter much, as multiple pending receives on the same socket are rare.
- If multiple concurrent receives or multiple concurrent sends are issued on the same socket, only the first will actually be cancelable.  This is because this implementation only plumbs the token through the SocketAsyncEventArgs-based code paths, not the APM based code paths, and currently when using the Task-based APIs, we use the SocketAsyncEventArgs under the covers for only one receive and one send at a time; other receives made while that SAEA receive is in progress or other sends made while that SAEA send is in progress will fall back to using the APM code paths.  This could be addressed in the future in various ways, including a) just using the SAEA code paths for all operations and deleting the APM fallback, or b) plumbing cancellation through APM as well.  However, for now, this approach addresses the primary use case and should be sufficient.
- This only affects code paths to which the CancellationToken passed to Send/ReceiveAsync could reach.  If in the future we add additional overloads taking CancellationToken, we will likely need to plumb it to more places.

Commit migrated from dotnet/corefx@2190a0f
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants