Add CancellationToken-accepting overloads for SendPingAsync#72338
Add CancellationToken-accepting overloads for SendPingAsync#72338rzikm merged 4 commits intodotnet:mainfrom
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
rzikm
left a comment
There was a problem hiding this comment.
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? |
rzikm
left a comment
There was a problem hiding this comment.
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; |
| stdout = await pingProcess.StandardOutput.ReadToEndAsync(timeoutOrCancellationToken).ConfigureAwait(false); | ||
| } | ||
| catch (TimeoutException) | ||
| catch when (timeoutOrCancellationToken.IsCancellationRequested) |
There was a problem hiding this comment.
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.
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.
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.
| _status = Disposed; | ||
| } | ||
|
|
||
| _timeoutOrCancellationSource?.Dispose(); |
There was a problem hiding this comment.
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.
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.
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.
It's unlikely to make a material difference, but it looks like this could use UnsafeRegister instead of Register.
| private static async Task<PingReply> SendIcmpEchoRequestOverRawSocketAsync(IPAddress address, byte[] buffer, int timeout, PingOptions? options) | ||
| private async Task<PingReply> SendIcmpEchoRequestOverRawSocketAsync(IPAddress address, byte[] buffer, int timeout, PingOptions? options) | ||
| { | ||
| CancellationToken timeoutOrCancellationToken = _timeoutOrCancellationSource!.Token; |
There was a problem hiding this comment.
Move this down into the try block?
| currentStatus = _status; | ||
| if (currentStatus == Free) | ||
| { | ||
| _timeoutOrCancellationSource ??= new(); |
There was a problem hiding this comment.
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.
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.
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.
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.
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
timeoutparameter 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.
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.
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? |
|
Thanks for the contribution! |
Provides "true" cancellation for async ping methods using either a token or
the existing SendAsyncCancel() API.
Fix #67260