diff --git a/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs index 8e665ceed..a5565f26b 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/IHttpDispatcher.cs @@ -1,9 +1,10 @@ using System.Net; +using System.Threading.Tasks; namespace NzbDrone.Common.Http.Dispatchers { public interface IHttpDispatcher { - HttpResponse GetResponse(HttpRequest request, CookieContainer cookies); + Task GetResponseAsync(HttpRequest request, CookieContainer cookies); } } diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 33a758c2e..3b880fc07 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Common.Http.Dispatchers _credentialCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "credentialcache"); } - public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) + public async Task GetResponseAsync(HttpRequest request, CookieContainer cookies) { var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url); requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent)); @@ -98,7 +98,7 @@ namespace NzbDrone.Common.Http.Dispatchers var httpClient = GetClient(request.Url); - using var responseMessage = httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token); + using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token); { byte[] data = null; @@ -106,7 +106,7 @@ namespace NzbDrone.Common.Http.Dispatchers { if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) { - responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token); + await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token); } else { diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 1e67641a9..4cbd2a448 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Threading.Tasks; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.EnvironmentInfo; @@ -25,6 +26,16 @@ namespace NzbDrone.Common.Http HttpResponse Post(HttpRequest request); HttpResponse Post(HttpRequest request) where T : new(); + + Task ExecuteAsync(HttpRequest request); + Task DownloadFileAsync(string url, string fileName); + Task GetAsync(HttpRequest request); + Task> GetAsync(HttpRequest request) + where T : new(); + Task HeadAsync(HttpRequest request); + Task PostAsync(HttpRequest request); + Task> PostAsync(HttpRequest request) + where T : new(); } public class HttpClient : IHttpClient @@ -52,11 +63,11 @@ namespace NzbDrone.Common.Http _cookieContainerCache = cacheManager.GetCache(typeof(HttpClient)); } - public HttpResponse Execute(HttpRequest request) + public virtual async Task ExecuteAsync(HttpRequest request) { var cookieContainer = InitializeRequestCookies(request); - var response = ExecuteRequest(request, cookieContainer); + var response = await ExecuteRequestAsync(request, cookieContainer); if (request.AllowAutoRedirect && response.HasHttpRedirect) { @@ -82,7 +93,7 @@ namespace NzbDrone.Common.Http request.ContentSummary = null; } - response = ExecuteRequest(request, cookieContainer); + response = await ExecuteRequestAsync(request, cookieContainer); } while (response.HasHttpRedirect); } @@ -112,6 +123,11 @@ namespace NzbDrone.Common.Http return response; } + public HttpResponse Execute(HttpRequest request) + { + return ExecuteAsync(request).GetAwaiter().GetResult(); + } + private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) { return statusCode switch @@ -122,7 +138,7 @@ namespace NzbDrone.Common.Http }; } - private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer) + private async Task ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer) { foreach (var interceptor in _requestInterceptors) { @@ -131,14 +147,14 @@ namespace NzbDrone.Common.Http if (request.RateLimit != TimeSpan.Zero) { - _rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimitKey, request.RateLimit); + await _rateLimitService.WaitAndPulseAsync(request.Url.Host, request.RateLimitKey, request.RateLimit); } _logger.Trace(request); var stopWatch = Stopwatch.StartNew(); - var response = _httpDispatcher.GetResponse(request, cookieContainer); + var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer); HandleResponseCookies(response, cookieContainer); @@ -246,7 +262,7 @@ namespace NzbDrone.Common.Http } } - public void DownloadFile(string url, string fileName) + public async Task DownloadFileAsync(string url, string fileName) { var fileNamePart = fileName + ".part"; @@ -261,12 +277,12 @@ namespace NzbDrone.Common.Http _logger.Debug("Downloading [{0}] to [{1}]", url, fileName); var stopWatch = Stopwatch.StartNew(); - using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite)) + await using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite)) { var request = new HttpRequest(url); request.AllowAutoRedirect = true; request.ResponseStream = fileStream; - var response = Get(request); + var response = await GetAsync(request); if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html")) { @@ -292,38 +308,71 @@ namespace NzbDrone.Common.Http } } - public HttpResponse Get(HttpRequest request) + public void DownloadFile(string url, string fileName) + { + // https://docs.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development#the-thread-pool-hack + Task.Run(() => DownloadFileAsync(url, fileName)).GetAwaiter().GetResult(); + } + + public Task GetAsync(HttpRequest request) { request.Method = HttpMethod.Get; - return Execute(request); + return ExecuteAsync(request); + } + + public HttpResponse Get(HttpRequest request) + { + return Task.Run(() => GetAsync(request)).GetAwaiter().GetResult(); + } + + public async Task> GetAsync(HttpRequest request) + where T : new() + { + var response = await GetAsync(request); + CheckResponseContentType(response); + return new HttpResponse(response); } public HttpResponse Get(HttpRequest request) where T : new() { - var response = Get(request); - CheckResponseContentType(response); - return new HttpResponse(response); + return Task.Run(() => GetAsync(request)).GetAwaiter().GetResult(); + } + + public Task HeadAsync(HttpRequest request) + { + request.Method = HttpMethod.Head; + return ExecuteAsync(request); } public HttpResponse Head(HttpRequest request) { - request.Method = HttpMethod.Head; - return Execute(request); + return Task.Run(() => HeadAsync(request)).GetAwaiter().GetResult(); + } + + public Task PostAsync(HttpRequest request) + { + request.Method = HttpMethod.Post; + return ExecuteAsync(request); } public HttpResponse Post(HttpRequest request) { - request.Method = HttpMethod.Post; - return Execute(request); + return Task.Run(() => PostAsync(request)).GetAwaiter().GetResult(); + } + + public async Task> PostAsync(HttpRequest request) + where T : new() + { + var response = await PostAsync(request); + CheckResponseContentType(response); + return new HttpResponse(response); } public HttpResponse Post(HttpRequest request) where T : new() { - var response = Post(request); - CheckResponseContentType(response); - return new HttpResponse(response); + return Task.Run(() => PostAsync(request)).GetAwaiter().GetResult(); } private void CheckResponseContentType(HttpResponse response) diff --git a/src/NzbDrone.Common/TPL/RateLimitService.cs b/src/NzbDrone.Common/TPL/RateLimitService.cs index 87b0ff22f..06bf24db7 100644 --- a/src/NzbDrone.Common/TPL/RateLimitService.cs +++ b/src/NzbDrone.Common/TPL/RateLimitService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Threading.Tasks; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -10,6 +11,7 @@ namespace NzbDrone.Common.TPL { void WaitAndPulse(string key, TimeSpan interval); void WaitAndPulse(string key, string subKey, TimeSpan interval); + Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval); } public class RateLimitService : IRateLimitService @@ -29,6 +31,28 @@ namespace NzbDrone.Common.TPL } public void WaitAndPulse(string key, string subKey, TimeSpan interval) + { + var delay = GetDelay(key, subKey, interval); + + if (delay.TotalSeconds > 0.0) + { + _logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds); + System.Threading.Thread.Sleep(delay); + } + } + + public async Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval) + { + var delay = GetDelay(key, subKey, interval); + + if (delay.TotalSeconds > 0.0) + { + _logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds); + await Task.Delay(delay); + } + } + + private TimeSpan GetDelay(string key, string subKey, TimeSpan interval) { var waitUntil = DateTime.UtcNow.Add(interval); @@ -59,13 +83,7 @@ namespace NzbDrone.Common.TPL waitUntil -= interval; - var delay = waitUntil - DateTime.UtcNow; - - if (delay.TotalSeconds > 0.0) - { - _logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds); - System.Threading.Thread.Sleep(delay); - } + return waitUntil - DateTime.UtcNow; } } }