Skip to content

Commit 5e7e76b

Browse files
ggnaegiraman-m
andauthored
#356 #695 #1924 Custom HttpMessageInvoker pooling (#1824)
* first version * first working version of the new http client pool * Some cleanup, removing classes that aren't used * some more cleanup * forgot passing PooledConnectionLifetime * adding todo for connection pool and request timeouts * some code cleanup * ongoing process refactoring tests * a little mistake with big effects * Several refactorings, disposing http response message to ensure that the connection is freed. Moving errors from Polly provider to Errors\QoS. * providing some comments * adding response body benchmark * some minor changes in MessageInvokerPool. * using context.RequestAborted in responder middleware (copying the response body from downstream service to the http context) * Fix style warnings * code review * moving response.Content.ReadAsStreamAsync nearer to CopyToAsync with using. Making sure, that the content stream is disposed * HttpResponse.Content never returns null (from .net 5 onwards) * adding more unit tests (validating, log warning if passthrough certificate, cookies are returned, message invoker timeout) * adding a tolerance margin * adding new acceptance test, checking memory usage. Needs to be compared with current implementation first. * Review tests by @raman-m * Update src/Ocelot/Configuration/HttpHandlerOptions.cs * Update src/Ocelot/Middleware/DownstreamResponse.cs * Update src/Ocelot/Requester/MessageInvokerPool.cs * Update src/Ocelot/Requester/HttpExceptionToErrorMapper.cs * Update src/Ocelot/Responder/HttpContextResponder.cs * Update src/Ocelot/Middleware/DownstreamResponse.cs * Use null-coalescing operator for `Nullable<int>` obj * some modifications in the acceptance test, adding a tolerance of 1 Mb * finalizing content tests. Making sure, that the downstream response body is not copied by the api gateway. * adapting tolerances * Disable StyleCop rule SA1010 which is in conflict with collection initialization block vs whitespace. More: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1010.md * Refactor Content tests --------- Co-authored-by: Raman Maksimchuk <[email protected]>
1 parent bb79587 commit 5e7e76b

40 files changed

+1192
-1386
lines changed

codeanalysis.ruleset

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Rule Id="SA1005" Action="None" />
1919
<Rule Id="SA1008" Action="None" />
2020
<Rule Id="SA1009" Action="None" />
21+
<Rule Id="SA1010" Action="None" />
2122
<Rule Id="SA1011" Action="None" />
2223
<Rule Id="SA1012" Action="None" />
2324
<Rule Id="SA1013" Action="None" />

src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Ocelot.Configuration;
44
using Ocelot.DependencyInjection;
55
using Ocelot.Errors;
6+
using Ocelot.Errors.QoS;
67
using Ocelot.Logging;
78
using Ocelot.Provider.Polly.Interfaces;
89
using Ocelot.Requester;

src/Ocelot.Provider.Polly/PollyQoSProvider.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public class PollyQoSProvider : IPollyQoSProvider<HttpResponseMessage>
1313
private readonly object _lockObject = new();
1414
private readonly IOcelotLogger _logger;
1515

16+
//todo: this should be configurable and available as global config parameter in ocelot.json
17+
public const int DefaultRequestTimeoutSeconds = 90;
18+
1619
private readonly HashSet<HttpStatusCode> _serverErrorCodes = new()
1720
{
1821
HttpStatusCode.InternalServerError,
@@ -63,14 +66,20 @@ private PollyPolicyWrapper<HttpResponseMessage> PollyPolicyWrapperFactory(Downst
6366
.Or<TimeoutException>()
6467
.CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking,
6568
durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak),
66-
onBreak: (ex, breakDelay) => _logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", ex.Exception),
69+
onBreak: (ex, breakDelay) =>
70+
_logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!",
71+
ex.Exception),
6772
onReset: () => _logger.LogDebug(info + "Call OK! Closed the circuit again."),
6873
onHalfOpen: () => _logger.LogDebug(info + "Half-open; Next call is a trial."));
6974
}
7075

76+
// No default set for polly timeout at the minute.
77+
// Since a user could potentially set timeout value = 0, we need to handle this case.
78+
// TODO throw an exception if the user sets timeout value = 0 or at least return a warning
79+
// TODO the design in DelegatingHandlerHandlerFactory should be reviewed
7180
var timeoutPolicy = Policy
7281
.TimeoutAsync<HttpResponseMessage>(
73-
TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue),
82+
TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue),
7483
TimeoutStrategy.Pessimistic);
7584

7685
return new PollyPolicyWrapper<HttpResponseMessage>(exceptionsAllowedBeforeBreakingPolicy, timeoutPolicy);

src/Ocelot.Provider.Polly/RequestTimedOutError.cs

-12
This file was deleted.

src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator
88
{
99
private readonly ITracer _tracer;
1010

11+
//todo: this should be configurable and available as global config parameter in ocelot.json
12+
public const int DefaultPooledConnectionLifetimeSeconds = 120;
13+
1114
public HttpHandlerOptionsCreator(IServiceProvider services)
1215
{
1316
_tracer = services.GetService<ITracer>();
@@ -19,9 +22,10 @@ public HttpHandlerOptions Create(FileHttpHandlerOptions options)
1922

2023
//be sure that maxConnectionPerServer is in correct range of values
2124
var maxConnectionPerServer = (options.MaxConnectionsPerServer > 0) ? options.MaxConnectionsPerServer : int.MaxValue;
25+
var pooledConnectionLifetime = TimeSpan.FromSeconds(options.PooledConnectionLifetimeSeconds ?? DefaultPooledConnectionLifetimeSeconds);
2226

2327
return new HttpHandlerOptions(options.AllowAutoRedirect,
24-
options.UseCookieContainer, useTracing, options.UseProxy, maxConnectionPerServer);
28+
options.UseCookieContainer, useTracing, options.UseProxy, maxConnectionPerServer, pooledConnectionLifetime);
2529
}
2630
}
2731
}

src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public FileHttpHandlerOptions()
88
MaxConnectionsPerServer = int.MaxValue;
99
UseCookieContainer = false;
1010
UseProxy = true;
11+
PooledConnectionLifetimeSeconds = null;
1112
}
1213

1314
public FileHttpHandlerOptions(FileHttpHandlerOptions from)
@@ -16,12 +17,14 @@ public FileHttpHandlerOptions(FileHttpHandlerOptions from)
1617
MaxConnectionsPerServer = from.MaxConnectionsPerServer;
1718
UseCookieContainer = from.UseCookieContainer;
1819
UseProxy = from.UseProxy;
20+
PooledConnectionLifetimeSeconds = from.PooledConnectionLifetimeSeconds;
1921
}
2022

2123
public bool AllowAutoRedirect { get; set; }
2224
public int MaxConnectionsPerServer { get; set; }
2325
public bool UseCookieContainer { get; set; }
2426
public bool UseProxy { get; set; }
25-
public bool UseTracing { get; set; }
27+
public bool UseTracing { get; set; }
28+
public int? PooledConnectionLifetimeSeconds { get; set; }
2629
}
2730
}
+23-16
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
namespace Ocelot.Configuration
22
{
33
/// <summary>
4-
/// Describes configuration parameters for http handler,
5-
/// that is created to handle a request to service.
4+
/// Describes configuration parameters for http handler, that is created to handle a request to service.
65
/// </summary>
76
public class HttpHandlerOptions
87
{
9-
public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool useTracing, bool useProxy, int maxConnectionsPerServer)
10-
{
11-
AllowAutoRedirect = allowAutoRedirect;
12-
UseCookieContainer = useCookieContainer;
13-
UseTracing = useTracing;
14-
UseProxy = useProxy;
15-
MaxConnectionsPerServer = maxConnectionsPerServer;
8+
public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool useTracing, bool useProxy,
9+
int maxConnectionsPerServer, TimeSpan pooledConnectionLifeTime)
10+
{
11+
AllowAutoRedirect = allowAutoRedirect;
12+
UseCookieContainer = useCookieContainer;
13+
UseTracing = useTracing;
14+
UseProxy = useProxy;
15+
MaxConnectionsPerServer = maxConnectionsPerServer;
16+
PooledConnectionLifeTime = pooledConnectionLifeTime;
1617
}
1718

1819
/// <summary>
1920
/// Specify if auto redirect is enabled.
2021
/// </summary>
2122
/// <value>AllowAutoRedirect.</value>
22-
public bool AllowAutoRedirect { get; }
23-
23+
public bool AllowAutoRedirect { get; }
24+
2425
/// <summary>
2526
/// Specify is handler has to use a cookie container.
2627
/// </summary>
@@ -31,18 +32,24 @@ public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool
3132
/// Specify is handler has to use a opentracing.
3233
/// </summary>
3334
/// <value>UseTracing.</value>
34-
public bool UseTracing { get; }
35-
35+
public bool UseTracing { get; }
36+
3637
/// <summary>
3738
/// Specify if handler has to use a proxy.
3839
/// </summary>
3940
/// <value>UseProxy.</value>
40-
public bool UseProxy { get; }
41-
41+
public bool UseProxy { get; }
42+
4243
/// <summary>
4344
/// Specify the maximum of concurrent connection to a network endpoint.
4445
/// </summary>
4546
/// <value>MaxConnectionsPerServer.</value>
4647
public int MaxConnectionsPerServer { get; }
48+
49+
/// <summary>
50+
/// Specify the maximum of time a connection can be pooled.
51+
/// </summary>
52+
/// <value>PooledConnectionLifeTime.</value>
53+
public TimeSpan PooledConnectionLifeTime { get; }
4754
}
48-
}
55+
}

src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Ocelot.Configuration
1+
using Ocelot.Configuration.Creator;
2+
3+
namespace Ocelot.Configuration
24
{
35
public class HttpHandlerOptionsBuilder
46
{
@@ -7,6 +9,7 @@ public class HttpHandlerOptionsBuilder
79
private bool _useTracing;
810
private bool _useProxy;
911
private int _maxConnectionPerServer;
12+
private TimeSpan _pooledConnectionLifetime = TimeSpan.FromSeconds(HttpHandlerOptionsCreator.DefaultPooledConnectionLifetimeSeconds);
1013

1114
public HttpHandlerOptionsBuilder WithAllowAutoRedirect(bool input)
1215
{
@@ -38,9 +41,15 @@ public HttpHandlerOptionsBuilder WithUseMaxConnectionPerServer(int maxConnection
3841
return this;
3942
}
4043

44+
public HttpHandlerOptionsBuilder WithPooledConnectionLifetimeSeconds(TimeSpan pooledConnectionLifetime)
45+
{
46+
_pooledConnectionLifetime = pooledConnectionLifetime;
47+
return this;
48+
}
49+
4150
public HttpHandlerOptions Build()
4251
{
43-
return new HttpHandlerOptions(_allowAutoRedirect, _useCookieContainer, _useTracing, _useProxy, _maxConnectionPerServer);
52+
return new HttpHandlerOptions(_allowAutoRedirect, _useCookieContainer, _useTracing, _useProxy, _maxConnectionPerServer, _pooledConnectionLifetime);
4453
}
4554
}
4655
}

src/Ocelot/DependencyInjection/OcelotBuilder.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,17 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
107107
Services.AddSingleton<IDownstreamRouteProvider, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();
108108
Services.AddSingleton<IDownstreamRouteProvider, DownstreamRouteCreator>();
109109
Services.TryAddSingleton<IDownstreamRouteProviderFactory, DownstreamRouteProviderFactory>();
110-
Services.TryAddSingleton<IHttpRequester, HttpClientHttpRequester>();
111110
Services.TryAddSingleton<IHttpResponder, HttpContextResponder>();
112111
Services.TryAddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
113112
Services.TryAddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
114-
Services.TryAddSingleton<IHttpClientCache, MemoryHttpClientCache>();
115113
Services.TryAddSingleton<IRequestMapper, RequestMapper>();
116114
Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>();
117115
Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
118116
Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
119117
Services.TryAddSingleton<ICacheKeyGenerator, DefaultCacheKeyGenerator>();
120118
Services.TryAddSingleton<IOcelotConfigurationChangeTokenSource, OcelotConfigurationChangeTokenSource>();
121119
Services.TryAddSingleton<IOptionsMonitor<IInternalConfiguration>, OcelotConfigurationMonitor>();
120+
Services.AddOcelotMessageInvokerPool();
122121

123122
// See this for why we register this as singleton:
124123
// http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using StatusCode = System.Net.HttpStatusCode;
2+
3+
namespace Ocelot.Errors.QoS;
4+
5+
public class RequestTimedOutError : Error
6+
{
7+
public RequestTimedOutError(Exception exception)
8+
: base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError, (int)StatusCode.ServiceUnavailable)
9+
{
10+
}
11+
}
+39-5
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
namespace Ocelot.Middleware
22
{
3-
public class DownstreamResponse
3+
public class DownstreamResponse : IDisposable
44
{
5-
public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, List<Header> headers, string reasonPhrase)
5+
// To detect redundant calls
6+
private bool _disposedValue;
7+
private readonly HttpResponseMessage _responseMessage;
8+
9+
public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, List<Header> headers,
10+
string reasonPhrase)
611
{
712
Content = content;
813
StatusCode = statusCode;
9-
Headers = headers ?? new List<Header>();
14+
Headers = headers ?? new();
1015
ReasonPhrase = reasonPhrase;
1116
}
1217

1318
public DownstreamResponse(HttpResponseMessage response)
14-
: this(response.Content, response.StatusCode, response.Headers.Select(x => new Header(x.Key, x.Value)).ToList(), response.ReasonPhrase)
19+
: this(response.Content, response.StatusCode,
20+
response.Headers.Select(x => new Header(x.Key, x.Value)).ToList(), response.ReasonPhrase)
1521
{
22+
_responseMessage = response;
1623
}
1724

18-
public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers, string reasonPhrase)
25+
public DownstreamResponse(HttpContent content, HttpStatusCode statusCode,
26+
IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers, string reasonPhrase)
1927
: this(content, statusCode, headers.Select(x => new Header(x.Key, x.Value)).ToList(), reasonPhrase)
2028
{
2129
}
@@ -24,5 +32,31 @@ public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, IEnume
2432
public HttpStatusCode StatusCode { get; }
2533
public List<Header> Headers { get; }
2634
public string ReasonPhrase { get; }
35+
36+
// Public implementation of Dispose pattern callable by consumers.
37+
public void Dispose()
38+
{
39+
Dispose(true);
40+
GC.SuppressFinalize(this);
41+
}
42+
43+
/// <summary>
44+
/// We should make sure we dispose the content and response message to close the connection to the downstream service.
45+
/// </summary>
46+
protected virtual void Dispose(bool disposing)
47+
{
48+
if (_disposedValue)
49+
{
50+
return;
51+
}
52+
53+
if (disposing)
54+
{
55+
Content?.Dispose();
56+
_responseMessage?.Dispose();
57+
}
58+
59+
_disposedValue = true;
60+
}
2761
}
2862
}

0 commit comments

Comments
 (0)