-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Add CancellationToken-accepting overloads for SendPingAsync #72338
Conversation
…work). Provides "true" cancellation for async ping methods using either a token or the existing SendAsyncCancel() API. Fix dotnet#67260
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @dotnet/ncl Issue DetailsProvides "true" cancellation for async ping methods using either a token or Fix #67260
|
src/libraries/System.Net.Ping/src/System/Net/NetworkInformation/Ping.PingUtility.cs
Show resolved
Hide resolved
src/libraries/System.Net.Ping/src/System/Net/NetworkInformation/Ping.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good, besides a few questions.
src/libraries/System.Net.Ping/src/System/Net/NetworkInformation/Ping.PingUtility.cs
Outdated
Show resolved
Hide resolved
@rzikm I thought I had asked about this in another comment, but I can't find it now. One callout is that this does not implement true cancellation on Windows because Windows pings use a native function which wasn't obviously cancelable from my read of the docs (let me know if I missed something). Maybe this is something that can be handled by CancelIOEx? One other option I considered was changing the Windows ping to use raw sockets when possible (as is done for Unix). However, I'm not sure of the pros/cons there. Thoughts? |
It would be nice to have consistent behavior between platforms if it can be achieved, I did a quick test and CancelIO didn't work (returned INVALID_HANDLE). Not sure if using raw ICMP sockets is viable, maybe @antonfirsov will know. |
I've not read the change yet. What do you do in this case? If it's just not as cancelable, e.g. once the native call is made the cancellation token is ignored, that's ok. If instead cancellation triggers the returned task to complete even while the native operation continues, that's not ok. |
IIRC, creating raw sockets requires elevated/admin privileges. If that's the case, we can't do that. |
It is much closer to the former than the latter but not exactly. Before this change, we already had a (mostly non-functional) version of cancellation via the
In Unix, we use the raw socket version conditionally based on whether we have permissions. I was proposing that we do the same thing for Windows. Maybe you're just saying that people running in admin mode is so rare that having this fork isn't worth it? Or maybe you're saying that the permission check for Unix doesn't work on Windows? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM now, but I would like @stephentoub to have a look as well in case I missed something.
using Process pingProcess = GetPingProcess(address, buffer, timeout, options); | ||
pingProcess.Start(); | ||
|
||
var timedOutOrCanceled = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var => bool
} | ||
catch (TimeoutException) | ||
catch when (timeoutOrCancellationToken.IsCancellationRequested) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to only catch OperationCanceledException? If the wait failed with an unrelated exception at appx the same time that cancellation was requested, we'll end up eating that exception and treating it as a timeout?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to make this change, just a few questions:
We don't want to only catch OperationCanceledException
Were you imagining
catch (OperationCanceledException) { ... }
or
catch (OperationCanceledException) when (timeoutOrCancellationToken.IsCancellationRequested) { ... }
If we go with the former it seems that there is some (maybe negligible) risk that something in the try block throws OCE for some reaons other than a passed-in cancellation token (a real-world example is HttpClient
throwing TaskCanceledException
when it times out).
Similarly, by catching only OCE is seems there is some risk that something we are calling will wrap the OCE (a real-world example is SendPingAsync()
prior to this PR.
Do either of these scenarios concern you?
If the wait failed with an unrelated exception at appx the same time that cancellation was requested, we'll end up eating that exception and treating it as a timeout
In that case, does it matter? We have a race condition where multiple things happened that should have aborted the ping and we are correctly informing the caller about one of those things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were you imagining
The latter.
In that case, does it matter? We have a race condition where multiple things happened that should have aborted the ping and we are correctly informing the caller about one of those things.
When this situation typically arises elsewhere, we make the same argument, but in those cases there's the added complication that the cancellation could have actually caused the other exception, and then it's actually preferable to show the cancelation one because the other is fake in a sense. Here, though, I don't think that's the case, in which case we generally want to prioritize real failures over manufactured ones from timeouts and cancellation, as they typically convey better information about the state of the system.
@@ -142,6 +149,8 @@ private void InternalDispose() | |||
_status = Disposed; | |||
} | |||
|
|||
_timeoutOrCancellationSource?.Dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dispose doesn't Cancel. Do you want to also Cancel? I'm just wondering about the comment on this method which suggests it does so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. I feel like this comment is just wrong. The way the method used to and still works is that if called while a ping request is in progress it sets _disposeRequested
to true and then returns. Then when the ping request completes it calls Finish
which completes the disposal.
It would be interesting if this comment implied that what InternalDisposeCore
does (disposing the handles) can actually safely cancel outstanding Windows ping requests, but given that the old .NET Framework version seems to have the same behavior I'm not convinced.
Do you agree? Should I just remove the comment? Should we change the code to start canceling the token once we've requested disposal to hurry outstanding requests along?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this comment is just wrong.
Looks like you're right. You can just delete it.
IPAddress[] addresses = await Dns.GetHostAddressesAsync(hostNameOrAddress).ConfigureAwait(false); | ||
Task<PingReply> pingReplyTask = SendPingAsyncCore(addresses[0], buffer, timeout, options); | ||
return await pingReplyTask.ConfigureAwait(false); | ||
using CancellationTokenRegistration _ = cancellationToken.Register(static state => ((Ping)state!).SetCanceled(), this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unlikely to make a material difference, but it looks like this could use UnsafeRegister instead of Register.
{ | ||
CancellationToken timeoutOrCancellationToken = _timeoutOrCancellationSource!.Token; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this down into the try block?
@@ -83,6 +85,7 @@ private void CheckStart() | |||
currentStatus = _status; | |||
if (currentStatus == Free) | |||
{ | |||
_timeoutOrCancellationSource ??= new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to mark this method as [MemberNotNull(nameof(_timeoutOrCancellationSource))]
, which should let you remove at least one if not more !
s on use of that field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean other methods? This is the one that lazily initializes the CancellationTokenSource
(Finish
then nulls it out if it has become canceled).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean putting that attribute on this method should let you remove the !
from other methods that call this one.
IPAddress address = await getAddress(getAddressArg, _timeoutOrCancellationSource!.Token).ConfigureAwait(false); | ||
|
||
Task<PingReply> pingTask = SendPingAsyncCore(address, buffer, timeout, options); | ||
_timeoutOrCancellationSource.CancelAfter(timeout); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing this here means the timeout won't apply to the GetHostAddressesAsync inside getAddress. Why is that ok / desirable? I'm also unclear as to why this is being done like this after the SendPingAsyncCore. Normally this kind of ordering would be to avoid a race condition, but that doesn't apply here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of these are attempts to maintain the existing behavior / be true to the documentation:
- The old code does not apply the timeout to
Dns.GetHostAddressesAsync
(see https://source.dot.net/#System.Net.Ping/System/Net/NetworkInformation/Ping.cs,646). - The documentation for the
timeout
parameter says: "The maximum number of milliseconds (after sending the echo message) to wait for the ICMP echo reply message".
I feel like the timeout is supposed to be inherent to the ping request, rather than being a general timeout for the entire operation. In contrast, I think the CancellationToken
is the way for the caller to abort the operation whenever they want. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll defer to @dotnet/ncl on the desired behavior here. I expect it was documented that way because there was literally no way before for the Dns operations to be timed out. But there is now. The question then is whether we want to update the timeout to apply to the whole operation or just to the waiting for the ICMP echo reply message; I'm ok either way.
I've not looked; are we consistent with the timeout only applying to the ICMP echo reply across all implementations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I feel like it is better to keep the timeout only for the ICMP echo reply. As noted, users can use the CancellationToken
to cancel the operation x ms from the start of it, and if we make the timeout
parameter cover the DNS resolution as well, users may get different results depending on whether DNS was fast enough or was/wasn't cached.
Thanks for all the feedback @rzikm @stephentoub . At this point I'm just waiting on resolutions to a few open threads: |
Looks like there is just one thing to address #72338 (comment) and we will be good to go, right? @madelson? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Thanks for the contribution! |
Provides "true" cancellation for async ping methods using either a token or
the existing SendAsyncCancel() API.
Fix #67260