[Mono.Android] Make HTTP connection attempt cancellable (#472)

The `HttpURLConnection.ConnectAsync` API doesn't accept a `CancellationToken`
instance and, thus, cannot be easily cancelled in a graceful manner. This
limitation resulted in using the `Task.WhenAny` to make sure the connection
attempt is aborted whenever the calling task is cancelled. This, however, led to
a problem of unobserved exceptions should the cancellation occur before the
connection attempt was successful.

This commit wraps the synchronous `HttpURLConnection.Connect` call in a task
that is passed the cancellation token, so that it can be gracefully cancelled
when needed. The cancellation is done by registering a handler for when token is
about to expire as Task.Run will check if cancellation was requested only before
it runs the passed code, the rest is the code's responsibility.
Since `URLConnection.Connect()` is asynchronous we abort it by calling
`Disconnect()` on the instance which is a brutal but effective way to interrupt
the connection attempt.

Additionally, the diff makes sure to properly configure a few tasks for `await`

Fixes https://bugzilla.xamarin.com/show_bug.cgi?id=51804
This commit is contained in:
Marek Habersack 2017-03-09 03:20:34 +01:00 коммит произвёл Jonathan Pryor
Родитель 9e6b04703b
Коммит 36d7e73de6
1 изменённых файлов: 19 добавлений и 18 удалений

Просмотреть файл

@ -209,8 +209,8 @@ namespace Xamarin.Android.Net
while (true) {
URL java_url = new URL (EncodeUrl (redirectState.NewUrl));
URLConnection java_connection = java_url.OpenConnection ();
HttpURLConnection httpConnection = await SetupRequestInternal (request, java_connection);
HttpResponseMessage response = await ProcessRequest (request, java_url, httpConnection, cancellationToken, redirectState);
HttpURLConnection httpConnection = await SetupRequestInternal (request, java_connection).ConfigureAwait (continueOnCapturedContext: false);;
HttpResponseMessage response = await ProcessRequest (request, java_url, httpConnection, cancellationToken, redirectState).ConfigureAwait (continueOnCapturedContext: false);;
if (response != null)
return response;
@ -235,6 +235,19 @@ namespace Xamarin.Android.Net
return Task.Run (() => httpConnection?.Disconnect ());
}
Task ConnectAsync (HttpURLConnection httpConnection, CancellationToken ct)
{
return Task.Run (() => {
try {
using (ct.Register (() => httpConnection?.Disconnect ()))
httpConnection?.Connect ();
} catch {
ct.ThrowIfCancellationRequested ();
throw;
}
}, ct);
}
async Task <HttpResponseMessage> DoProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
{
if (Logger.LogNet)
@ -247,20 +260,11 @@ namespace Xamarin.Android.Net
cancellationToken.ThrowIfCancellationRequested ();
}
CancellationTokenSource waitHandleSource = new CancellationTokenSource();
try {
if (Logger.LogNet)
Logger.Log (LogLevel.Info, LOG_APP, $" connecting");
CancellationToken linkedToken = CancellationTokenSource.CreateLinkedTokenSource(waitHandleSource.Token, cancellationToken).Token;
await Task.WhenAny (
httpConnection.ConnectAsync (),
Task.Run (()=> {
linkedToken.WaitHandle.WaitOne();
if (Logger.LogNet)
Logger.Log(LogLevel.Info, LOG_APP, $"Wait handle task finished");
}))
.ConfigureAwait(false);
await ConnectAsync (httpConnection, cancellationToken).ConfigureAwait (continueOnCapturedContext: false);
if (Logger.LogNet)
Logger.Log (LogLevel.Info, LOG_APP, $" connected");
} catch (Java.Net.ConnectException ex) {
@ -268,9 +272,6 @@ namespace Xamarin.Android.Net
Logger.Log (LogLevel.Info, LOG_APP, $"Connection exception {ex}");
// Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler
throw new WebException (ex.Message, ex, WebExceptionStatus.ConnectFailure, null);
} finally{
//If not already cancelled, cancel the WaitOne through the waitHandleSource to prevent an orphaned thread
waitHandleSource.Cancel();
}
if (cancellationToken.IsCancellationRequested) {
@ -541,7 +542,7 @@ namespace Xamarin.Android.Net
/// <param name="conn">Pre-configured connection instance</param>
protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn)
{
return Task.Factory.StartNew (AssertSelf);
return Task.Run (AssertSelf);
}
/// <summary>
@ -645,9 +646,9 @@ namespace Xamarin.Android.Net
}
HandlePreAuthentication (httpConnection);
await SetupRequest (request, httpConnection);
await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);;
SetupRequestBody (httpConnection, request);
return httpConnection;
}