New: Async HttpClient

This commit is contained in:
Qstick 2023-07-16 12:18:30 -05:00
parent f62bc59a73
commit 9bd2cbca54
4 changed files with 100 additions and 32 deletions

View File

@ -1,9 +1,10 @@
using System.Net; using System.Net;
using System.Threading.Tasks;
namespace NzbDrone.Common.Http.Dispatchers namespace NzbDrone.Common.Http.Dispatchers
{ {
public interface IHttpDispatcher public interface IHttpDispatcher
{ {
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies); Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
} }
} }

View File

@ -43,7 +43,7 @@ namespace NzbDrone.Common.Http.Dispatchers
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache"); _credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
} }
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{ {
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url); var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent)); requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
@ -98,7 +98,7 @@ namespace NzbDrone.Common.Http.Dispatchers
var httpClient = GetClient(request.Url); 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; byte[] data = null;
@ -106,7 +106,7 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK) 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 else
{ {

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -25,6 +26,16 @@ namespace NzbDrone.Common.Http
HttpResponse Post(HttpRequest request); HttpResponse Post(HttpRequest request);
HttpResponse<T> Post<T>(HttpRequest request) HttpResponse<T> Post<T>(HttpRequest request)
where T : new(); where T : new();
Task<HttpResponse> ExecuteAsync(HttpRequest request);
Task DownloadFileAsync(string url, string fileName);
Task<HttpResponse> GetAsync(HttpRequest request);
Task<HttpResponse<T>> GetAsync<T>(HttpRequest request)
where T : new();
Task<HttpResponse> HeadAsync(HttpRequest request);
Task<HttpResponse> PostAsync(HttpRequest request);
Task<HttpResponse<T>> PostAsync<T>(HttpRequest request)
where T : new();
} }
public class HttpClient : IHttpClient public class HttpClient : IHttpClient
@ -52,11 +63,11 @@ namespace NzbDrone.Common.Http
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient)); _cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
} }
public HttpResponse Execute(HttpRequest request) public virtual async Task<HttpResponse> ExecuteAsync(HttpRequest request)
{ {
var cookieContainer = InitializeRequestCookies(request); var cookieContainer = InitializeRequestCookies(request);
var response = ExecuteRequest(request, cookieContainer); var response = await ExecuteRequestAsync(request, cookieContainer);
if (request.AllowAutoRedirect && response.HasHttpRedirect) if (request.AllowAutoRedirect && response.HasHttpRedirect)
{ {
@ -82,7 +93,7 @@ namespace NzbDrone.Common.Http
request.ContentSummary = null; request.ContentSummary = null;
} }
response = ExecuteRequest(request, cookieContainer); response = await ExecuteRequestAsync(request, cookieContainer);
} }
while (response.HasHttpRedirect); while (response.HasHttpRedirect);
} }
@ -112,6 +123,11 @@ namespace NzbDrone.Common.Http
return response; return response;
} }
public HttpResponse Execute(HttpRequest request)
{
return ExecuteAsync(request).GetAwaiter().GetResult();
}
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{ {
return statusCode switch return statusCode switch
@ -122,7 +138,7 @@ namespace NzbDrone.Common.Http
}; };
} }
private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer) private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{ {
foreach (var interceptor in _requestInterceptors) foreach (var interceptor in _requestInterceptors)
{ {
@ -131,14 +147,14 @@ namespace NzbDrone.Common.Http
if (request.RateLimit != TimeSpan.Zero) 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); _logger.Trace(request);
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
var response = _httpDispatcher.GetResponse(request, cookieContainer); var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, 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"; var fileNamePart = fileName + ".part";
@ -261,12 +277,12 @@ namespace NzbDrone.Common.Http
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName); _logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew(); 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); var request = new HttpRequest(url);
request.AllowAutoRedirect = true; request.AllowAutoRedirect = true;
request.ResponseStream = fileStream; request.ResponseStream = fileStream;
var response = Get(request); var response = await GetAsync(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html")) 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<HttpResponse> GetAsync(HttpRequest request)
{ {
request.Method = HttpMethod.Get; 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<HttpResponse<T>> GetAsync<T>(HttpRequest request)
where T : new()
{
var response = await GetAsync(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
} }
public HttpResponse<T> Get<T>(HttpRequest request) public HttpResponse<T> Get<T>(HttpRequest request)
where T : new() where T : new()
{ {
var response = Get(request); return Task.Run(() => GetAsync<T>(request)).GetAwaiter().GetResult();
CheckResponseContentType(response); }
return new HttpResponse<T>(response);
public Task<HttpResponse> HeadAsync(HttpRequest request)
{
request.Method = HttpMethod.Head;
return ExecuteAsync(request);
} }
public HttpResponse Head(HttpRequest request) public HttpResponse Head(HttpRequest request)
{ {
request.Method = HttpMethod.Head; return Task.Run(() => HeadAsync(request)).GetAwaiter().GetResult();
return Execute(request); }
public Task<HttpResponse> PostAsync(HttpRequest request)
{
request.Method = HttpMethod.Post;
return ExecuteAsync(request);
} }
public HttpResponse Post(HttpRequest request) public HttpResponse Post(HttpRequest request)
{ {
request.Method = HttpMethod.Post; return Task.Run(() => PostAsync(request)).GetAwaiter().GetResult();
return Execute(request); }
public async Task<HttpResponse<T>> PostAsync<T>(HttpRequest request)
where T : new()
{
var response = await PostAsync(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
} }
public HttpResponse<T> Post<T>(HttpRequest request) public HttpResponse<T> Post<T>(HttpRequest request)
where T : new() where T : new()
{ {
var response = Post(request); return Task.Run(() => PostAsync<T>(request)).GetAwaiter().GetResult();
CheckResponseContentType(response);
return new HttpResponse<T>(response);
} }
private void CheckResponseContentType(HttpResponse response) private void CheckResponseContentType(HttpResponse response)

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -10,6 +11,7 @@ namespace NzbDrone.Common.TPL
{ {
void WaitAndPulse(string key, TimeSpan interval); void WaitAndPulse(string key, TimeSpan interval);
void WaitAndPulse(string key, string subKey, TimeSpan interval); void WaitAndPulse(string key, string subKey, TimeSpan interval);
Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval);
} }
public class RateLimitService : IRateLimitService public class RateLimitService : IRateLimitService
@ -29,6 +31,28 @@ namespace NzbDrone.Common.TPL
} }
public void WaitAndPulse(string key, string subKey, TimeSpan interval) 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); var waitUntil = DateTime.UtcNow.Add(interval);
@ -59,13 +83,7 @@ namespace NzbDrone.Common.TPL
waitUntil -= interval; waitUntil -= interval;
var delay = waitUntil - DateTime.UtcNow; return 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);
}
} }
} }
} }