[Foundation] Break out of the delegate when we have canceled a task. Fixes #9132 - #10230
Conversation
…ixes dotnet#9132 This is an interesting issue that happens in the following scenario: 1. A request is performed that will take some extra time to get a response or an error. 2. The request is cancelled BEFORE the delegate methods DidReceiveResponse, DidReceiveData or DidCompleteWithError and called. Yet we have not yet fully canceled the native request. It does take some time to do so, that is why we have a 'NSURLSessionTaskStateCanceling' and not 'NSURLSessionTaskStateCancelled' The issue happens because the GetInflightData checks if the task has been canceled. In that method we have the following: ```csharp if (inflight.CancellationToken.IsCancellationRequested) task?.Cancel (); return inflight; ``` The call of the Cancel method in the NSData task has as ripple effect which is the execution of the following callback: ```csharp cancellationToken.Register (() => { RemoveInflightData (dataTask); tcs.TrySetCanceled (); }); ``` Which calls: ```csharp void RemoveInflightData (NSUrlSessionTask task, bool cancel = true) { lock (inflightRequestsLock) { if (inflightRequests.TryGetValue (task, out var data)) { if (cancel) data.CancellationTokenSource.Cancel (); data.Dispose (); inflightRequests.Remove (task); } // do we need to be notified? If we have not inflightData, we do not if (inflightRequests.Count == 0) RemoveNotification (); } if (cancel) task?.Cancel (); task?.Dispose (); } ``` The above call does call Dispose and Dipose in the inflight data does: ```csharp if (disposing) { if (CancellationTokenSource != null) { CancellationTokenSource.Dispose (); CancellationTokenSource = null; } } ``` If we follow these set of events for example in the DidRecieveResponse: ```chsarp [Preserve (Conditional = true)] public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action<NSUrlSessionResponseDisposition> completionHandler) { var inflight = GetInflightData (dataTask); if (inflight == null) return; try { var urlResponse = (NSHttpUrlResponse)response; var status = (int)urlResponse.StatusCode; var content = new NSUrlSessionDataTaskStreamContent (inflight.Stream, () => { if (!inflight.Completed) { dataTask.Cancel (); } inflight.Disposed = true; inflight.Stream.TrySetException (new ObjectDisposedException ("The content stream was disposed.")); sessionHandler.RemoveInflightData (dataTask); }, inflight.CancellationTokenSource.Token); ``` If can happen that, when we get the delegate call to the method, the request has been cancelled. That means that we are in the precise state in which we do not longer care about the response BUT we did get a infligh data reference.. that HAS BEEN DIPOSED!!! this later gives a NRE in several places. The correct way is to return null from the GetInflighData method if we have been cancelled, this makes sense because if we cancelled we do not care about the execution of any of the methods that required the data since it has been disposed!
spouliot
left a comment
There was a problem hiding this comment.
Let's get confirmation it fix the customers issues before backporting to release branches (since it looks unlikely to get a [unit] test for this)
| // | ||
| // DidReceiveResponse We might have received a response, but either the user cancelled or a | ||
| // timeout did, if that is the case, we do not care about the respose. | ||
| // DidReceiveData Of buffer has a partial response ergo garbage and there is not real |
There was a problem hiding this comment.
ergo ? we're now using latin for comments :)
There was a problem hiding this comment.
We are that posh :)
| // we did not find the data. | ||
| if (inflight.CancellationToken.IsCancellationRequested) { | ||
| task?.Cancel (); | ||
| // return null so that we break out of any deleate method. |
|
@spouliot I'll trigger a pkg build for this. |
|
Build failure |
|
Build failure |
|
Watch issues are known and have been fixed on #10235 |
|
Build failure |
|
Is there any estimate on when will this fix be released ? It's causing us many crashes |
|
@ahhassan3 we have not landed it because we did not had customers confirming the fix. |
|
@mandel-macaque This bug is somewhat difficult to reproduce on demand but we have ad over 70 counts of crashes from the customer who use our app because of this issue. I saw that you have merged it on to the main code branch, do you know when the xamarin.forms nuget will be updated with this fix in it. Thank you |
…ixes dotnet#9132 (dotnet#10230) This is an interesting issue that happens in the following scenario: 1. A request is performed that will take some extra time to get a response or an error. 2. The request is cancelled BEFORE the delegate methods DidReceiveResponse, DidReceiveData or DidCompleteWithError and called. Yet we have not yet fully canceled the native request. It does take some time to do so, that is why we have a 'NSURLSessionTaskStateCanceling' and not 'NSURLSessionTaskStateCancelled' The issue happens because the GetInflightData checks if the task has been canceled. In that method we have the following: ```csharp if (inflight.CancellationToken.IsCancellationRequested) task?.Cancel (); return inflight; ``` The call of the Cancel method in the NSData task has as ripple effect which is the execution of the following callback: ```csharp cancellationToken.Register (() => { RemoveInflightData (dataTask); tcs.TrySetCanceled (); }); ``` Which calls: ```csharp void RemoveInflightData (NSUrlSessionTask task, bool cancel = true) { lock (inflightRequestsLock) { if (inflightRequests.TryGetValue (task, out var data)) { if (cancel) data.CancellationTokenSource.Cancel (); data.Dispose (); inflightRequests.Remove (task); } // do we need to be notified? If we have not inflightData, we do not if (inflightRequests.Count == 0) RemoveNotification (); } if (cancel) task?.Cancel (); task?.Dispose (); } ``` The above call does call Dispose and Dipose in the inflight data does: ```csharp if (disposing) { if (CancellationTokenSource != null) { CancellationTokenSource.Dispose (); CancellationTokenSource = null; } } ``` If we follow these set of events for example in the DidRecieveResponse: ```chsarp [Preserve (Conditional = true)] public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action<NSUrlSessionResponseDisposition> completionHandler) { var inflight = GetInflightData (dataTask); if (inflight == null) return; try { var urlResponse = (NSHttpUrlResponse)response; var status = (int)urlResponse.StatusCode; var content = new NSUrlSessionDataTaskStreamContent (inflight.Stream, () => { if (!inflight.Completed) { dataTask.Cancel (); } inflight.Disposed = true; inflight.Stream.TrySetException (new ObjectDisposedException ("The content stream was disposed.")); sessionHandler.RemoveInflightData (dataTask); }, inflight.CancellationTokenSource.Token); ``` If can happen that, when we get the delegate call to the method, the request has been cancelled. That means that we are in the precise state in which we do not longer care about the response BUT we did get a infligh data reference.. that HAS BEEN DIPOSED!!! this later gives a NRE in several places. The correct way is to return null from the GetInflighData method if we have been cancelled, this makes sense because if we cancelled we do not care about the execution of any of the methods that required the data since it has been disposed! Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
…ixes #9132 (#10230) (#10930) This is an interesting issue that happens in the following scenario: 1. A request is performed that will take some extra time to get a response or an error. 2. The request is cancelled BEFORE the delegate methods DidReceiveResponse, DidReceiveData or DidCompleteWithError and called. Yet we have not yet fully canceled the native request. It does take some time to do so, that is why we have a 'NSURLSessionTaskStateCanceling' and not 'NSURLSessionTaskStateCancelled' The issue happens because the GetInflightData checks if the task has been canceled. In that method we have the following: ```csharp if (inflight.CancellationToken.IsCancellationRequested) task?.Cancel (); return inflight; ``` The call of the Cancel method in the NSData task has as ripple effect which is the execution of the following callback: ```csharp cancellationToken.Register (() => { RemoveInflightData (dataTask); tcs.TrySetCanceled (); }); ``` Which calls: ```csharp void RemoveInflightData (NSUrlSessionTask task, bool cancel = true) { lock (inflightRequestsLock) { if (inflightRequests.TryGetValue (task, out var data)) { if (cancel) data.CancellationTokenSource.Cancel (); data.Dispose (); inflightRequests.Remove (task); } // do we need to be notified? If we have not inflightData, we do not if (inflightRequests.Count == 0) RemoveNotification (); } if (cancel) task?.Cancel (); task?.Dispose (); } ``` The above call does call Dispose and Dipose in the inflight data does: ```csharp if (disposing) { if (CancellationTokenSource != null) { CancellationTokenSource.Dispose (); CancellationTokenSource = null; } } ``` If we follow these set of events for example in the DidRecieveResponse: ```chsarp [Preserve (Conditional = true)] public override void DidReceiveResponse (NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action<NSUrlSessionResponseDisposition> completionHandler) { var inflight = GetInflightData (dataTask); if (inflight == null) return; try { var urlResponse = (NSHttpUrlResponse)response; var status = (int)urlResponse.StatusCode; var content = new NSUrlSessionDataTaskStreamContent (inflight.Stream, () => { if (!inflight.Completed) { dataTask.Cancel (); } inflight.Disposed = true; inflight.Stream.TrySetException (new ObjectDisposedException ("The content stream was disposed.")); sessionHandler.RemoveInflightData (dataTask); }, inflight.CancellationTokenSource.Token); ``` If can happen that, when we get the delegate call to the method, the request has been cancelled. That means that we are in the precise state in which we do not longer care about the response BUT we did get a infligh data reference.. that HAS BEEN DIPOSED!!! this later gives a NRE in several places. The correct way is to return null from the GetInflighData method if we have been cancelled, this makes sense because if we cancelled we do not care about the execution of any of the methods that required the data since it has been disposed! Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
This is an interesting issue that happens in the following scenario:
response or an error.
DidReceiveResponse, DidReceiveData or DidCompleteWithError and
called. Yet we have not yet fully canceled the native request. It
does take some time to do so, that is why we have a
'NSURLSessionTaskStateCanceling' and not 'NSURLSessionTaskStateCancelled'
The issue happens because the GetInflightData checks if the task has
been canceled. In that method we have the following:
The call of the Cancel method in the NSData task has as ripple effect which
is the execution of the following callback:
Which calls:
The above call does call Dispose and Dipose in the inflight data does:
If we follow these set of events for example in the
DidRecieveResponse:
If can happen that, when we get the delegate call to the method, the
request has been cancelled. That means that we are in the precise state
in which we do not longer care about the response BUT we did get a
infligh data reference.. that HAS BEEN DISPOSED!!! this later gives a NRE
in several places.
The correct way is to return null from the GetInflighData method if we
have been cancelled, this makes sense because if we cancelled we do not
care about the execution of any of the methods that required the data
since it has been disposed!
fixes #9132