Feature/timeout for http client (#319)

* #318 http client obeys Qos timeout or defaults to 90 seconds, which is think is default for http client anyway but zero docs....

* #318 updated docs to specify default timeout and make it clear how to set it on a ReRoute basis

* #318 missed this

* #318 missed this
This commit is contained in:
Tom Pallister 2018-04-18 15:24:16 +01:00 committed by GitHub
parent f9dc8659c0
commit 5e1605882b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 236 additions and 51 deletions

View File

@ -17,6 +17,17 @@ Add the following section to a ReRoute configuration.
You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be
implemented. Duration of break is how long the circuit breaker will stay open for after it is tripped. implemented. Duration of break is how long the circuit breaker will stay open for after it is tripped.
TimeoutValue means ff a request takes more than 5 seconds it will automatically be timed out. TimeoutValue means if a request takes more than 5 seconds it will automatically be timed out.
If you do not add a QoS section QoS will not be used. You can set the TimeoutValue in isoldation of the ExceptionsAllowedBeforeBreaking and DurationOfBreak options.
.. code-block:: json
"QoSOptions": {
"TimeoutValue":5000
}
There is no point setting the other two in isolation as they affect each other :)
If you do not add a QoS section QoS will not be used however Ocelot will default to a 90 second timeout
on all downstream requests. If someone needs this to be configurable open an issue.

View File

@ -48,5 +48,6 @@ namespace Ocelot.Configuration.File
public string Key { get;set; } public string Key { get;set; }
public List<string> DelegatingHandlers {get;set;} public List<string> DelegatingHandlers {get;set;}
public int Priority { get;set; } public int Priority { get;set; }
public int Timeout { get; set; }
} }
} }

View File

@ -16,12 +16,12 @@ namespace Ocelot.Configuration
TimeoutStrategy = timeoutStrategy; TimeoutStrategy = timeoutStrategy;
} }
public int ExceptionsAllowedBeforeBreaking { get; private set; } public int ExceptionsAllowedBeforeBreaking { get; }
public int DurationOfBreak { get; private set; } public int DurationOfBreak { get; }
public int TimeoutValue { get; private set; } public int TimeoutValue { get; }
public TimeoutStrategy TimeoutStrategy { get; private set; } public TimeoutStrategy TimeoutStrategy { get; }
} }
} }

View File

@ -17,6 +17,7 @@ namespace Ocelot.Requester
private HttpClient _httpClient; private HttpClient _httpClient;
private IHttpClient _client; private IHttpClient _client;
private HttpClientHandler _httpclientHandler; private HttpClientHandler _httpclientHandler;
private readonly TimeSpan _defaultTimeout;
public HttpClientBuilder( public HttpClientBuilder(
IDelegatingHandlerHandlerFactory factory, IDelegatingHandlerHandlerFactory factory,
@ -26,6 +27,10 @@ namespace Ocelot.Requester
_factory = factory; _factory = factory;
_cacheHandlers = cacheHandlers; _cacheHandlers = cacheHandlers;
_logger = logger; _logger = logger;
// This is hardcoded at the moment but can easily be added to configuration
// if required by a user request.
_defaultTimeout = TimeSpan.FromSeconds(90);
} }
public IHttpClient Create(DownstreamContext request) public IHttpClient Create(DownstreamContext request)
@ -46,7 +51,14 @@ namespace Ocelot.Requester
CookieContainer = new CookieContainer() CookieContainer = new CookieContainer()
}; };
_httpClient = new HttpClient(CreateHttpMessageHandler(_httpclientHandler, request.DownstreamReRoute)); var timeout = request.DownstreamReRoute.QosOptionsOptions.TimeoutValue == 0
? _defaultTimeout
: TimeSpan.FromMilliseconds(request.DownstreamReRoute.QosOptionsOptions.TimeoutValue);
_httpClient = new HttpClient(CreateHttpMessageHandler(_httpclientHandler, request.DownstreamReRoute))
{
Timeout = timeout
};
_client = new HttpClientWrapper(_httpClient); _client = new HttpClientWrapper(_httpClient);

View File

@ -39,6 +39,10 @@ namespace Ocelot.Requester
{ {
return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception)); return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
} }
catch (TaskCanceledException exception)
{
return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
}
catch (BrokenCircuitException exception) catch (BrokenCircuitException exception)
{ {
return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception)); return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));

View File

@ -14,6 +14,8 @@ using static Rafty.Infrastructure.Wait;
namespace Ocelot.AcceptanceTests namespace Ocelot.AcceptanceTests
{ {
using Xunit.Abstractions;
public class ButterflyTracingTests : IDisposable public class ButterflyTracingTests : IDisposable
{ {
private IWebHost _serviceOneBuilder; private IWebHost _serviceOneBuilder;
@ -23,9 +25,11 @@ namespace Ocelot.AcceptanceTests
private string _downstreamPathOne; private string _downstreamPathOne;
private string _downstreamPathTwo; private string _downstreamPathTwo;
private int _butterflyCalled; private int _butterflyCalled;
private readonly ITestOutputHelper _output;
public ButterflyTracingTests() public ButterflyTracingTests(ITestOutputHelper output)
{ {
_output = output;
_steps = new Steps(); _steps = new Steps();
} }
@ -104,7 +108,9 @@ namespace Ocelot.AcceptanceTests
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
.BDDfy(); .BDDfy();
var commandOnAllStateMachines = WaitFor(10000).Until(() => _butterflyCalled == 4); var commandOnAllStateMachines = WaitFor(10000).Until(() => _butterflyCalled >= 4);
_output.WriteLine($"_butterflyCalled is {_butterflyCalled}");
commandOnAllStateMachines.ShouldBeTrue(); commandOnAllStateMachines.ShouldBeTrue();
} }

View File

@ -25,6 +25,82 @@ namespace Ocelot.AcceptanceTests
_steps = new Steps(); _steps = new Steps();
} }
[Fact]
public void should_not_timeout()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51569,
}
},
DownstreamScheme = "http",
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Post" },
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1000,
}
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 10))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.And(x => _steps.GivenThePostHasContent("postContent"))
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.BDDfy();
}
[Fact]
public void should_timeout()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51579,
}
},
DownstreamScheme = "http",
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Post" },
QoSOptions = new FileQoSOptions
{
TimeoutValue = 10,
}
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.And(x => _steps.GivenThePostHasContent("postContent"))
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
.BDDfy();
}
[Fact] [Fact]
public void should_open_circuit_breaker_then_close() public void should_open_circuit_breaker_then_close()
{ {
@ -122,7 +198,7 @@ namespace Ocelot.AcceptanceTests
}; };
this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura")) this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura"))
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom")) .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom", 0))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .And(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
@ -193,7 +269,7 @@ namespace Ocelot.AcceptanceTests
_brokenService.Start(); _brokenService.Start();
} }
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout)
{ {
_workingService = new WebHostBuilder() _workingService = new WebHostBuilder()
.UseUrls(url) .UseUrls(url)
@ -205,6 +281,7 @@ namespace Ocelot.AcceptanceTests
{ {
app.Run(async context => app.Run(async context =>
{ {
Thread.Sleep(timeout);
context.Response.StatusCode = statusCode; context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody); await context.Response.WriteAsync(responseBody);
}); });

View File

@ -1,5 +1,20 @@
{ {
"ReRoutes": [ "ReRoutes": [
{
"DownstreamPathTemplate": "/profile",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/profile",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 3000
}
],
"QoSOptions": {
"TimeoutValue": 360000
}
},
{ {
"DownstreamPathTemplate": "/api/values", "DownstreamPathTemplate": "/api/values",
"DownstreamScheme": "http", "DownstreamScheme": "http",

View File

@ -50,6 +50,7 @@ namespace Ocelot.UnitTests.Requester
.WithIsQos(false) .WithIsQos(false)
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false))
.WithReRouteKey("") .WithReRouteKey("")
.WithQosOptions(new QoSOptionsBuilder().Build())
.Build(); .Build();
this.Given(x => GivenTheFactoryReturns()) this.Given(x => GivenTheFactoryReturns())
@ -66,6 +67,7 @@ namespace Ocelot.UnitTests.Requester
.WithIsQos(false) .WithIsQos(false)
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false))
.WithReRouteKey("") .WithReRouteKey("")
.WithQosOptions(new QoSOptionsBuilder().Build())
.Build(); .Build();
var fakeOne = new FakeDelegatingHandler(); var fakeOne = new FakeDelegatingHandler();
@ -93,6 +95,7 @@ namespace Ocelot.UnitTests.Requester
.WithIsQos(false) .WithIsQos(false)
.WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false)) .WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false))
.WithReRouteKey("") .WithReRouteKey("")
.WithQosOptions(new QoSOptionsBuilder().Build())
.Build(); .Build();
this.Given(_ => GivenADownstreamService()) this.Given(_ => GivenADownstreamService())

View File

@ -21,7 +21,7 @@ namespace Ocelot.UnitTests.Requester
public class HttpClientHttpRequesterTest public class HttpClientHttpRequesterTest
{ {
private readonly Mock<IHttpClientCache> _cacheHandlers; private readonly Mock<IHttpClientCache> _cacheHandlers;
private Mock<IDelegatingHandlerHandlerFactory> _factory; private readonly Mock<IDelegatingHandlerHandlerFactory> _factory;
private Response<HttpResponseMessage> _response; private Response<HttpResponseMessage> _response;
private readonly HttpClientHttpRequester _httpClientRequester; private readonly HttpClientHttpRequester _httpClientRequester;
private DownstreamContext _request; private DownstreamContext _request;
@ -47,8 +47,12 @@ namespace Ocelot.UnitTests.Requester
[Fact] [Fact]
public void should_call_request_correctly() public void should_call_request_correctly()
{ {
var reRoute = new DownstreamReRouteBuilder().WithIsQos(false) var reRoute = new DownstreamReRouteBuilder()
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)).WithReRouteKey("").Build(); .WithIsQos(false)
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false))
.WithReRouteKey("")
.WithQosOptions(new QoSOptionsBuilder().Build())
.Build();
var context = new DownstreamContext(new DefaultHttpContext()) var context = new DownstreamContext(new DefaultHttpContext())
{ {
@ -66,8 +70,12 @@ namespace Ocelot.UnitTests.Requester
[Fact] [Fact]
public void should_call_request_unable_to_complete_request() public void should_call_request_unable_to_complete_request()
{ {
var reRoute = new DownstreamReRouteBuilder().WithIsQos(false) var reRoute = new DownstreamReRouteBuilder()
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)).WithReRouteKey("").Build(); .WithIsQos(false)
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false))
.WithReRouteKey("")
.WithQosOptions(new QoSOptionsBuilder().Build())
.Build();
var context = new DownstreamContext(new DefaultHttpContext()) var context = new DownstreamContext(new DefaultHttpContext())
{ {
@ -81,6 +89,30 @@ namespace Ocelot.UnitTests.Requester
.BDDfy(); .BDDfy();
} }
[Fact]
public void http_client_request_times_out()
{
var reRoute = new DownstreamReRouteBuilder()
.WithIsQos(false)
.WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false))
.WithReRouteKey("")
.WithQosOptions(new QoSOptionsBuilder().WithTimeoutValue(1).Build())
.Build();
var context = new DownstreamContext(new DefaultHttpContext())
{
DownstreamReRoute = reRoute,
DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }),
};
this.Given(_ => GivenTheRequestIs(context))
.And(_ => GivenTheHouseReturnsTimeoutHandler())
.When(_ => WhenIGetResponse())
.Then(_ => ThenTheResponseIsCalledError())
.And(_ => ThenTheErrorIsTimeout())
.BDDfy();
}
private void GivenTheRequestIs(DownstreamContext request) private void GivenTheRequestIs(DownstreamContext request)
{ {
_request = request; _request = request;
@ -101,6 +133,11 @@ namespace Ocelot.UnitTests.Requester
_response.IsError.ShouldBeTrue(); _response.IsError.ShouldBeTrue();
} }
private void ThenTheErrorIsTimeout()
{
_response.Errors[0].ShouldBeOfType<RequestTimedOutError>();
}
private void GivenTheHouseReturnsOkHandler() private void GivenTheHouseReturnsOkHandler()
{ {
var handlers = new List<Func<DelegatingHandler>> var handlers = new List<Func<DelegatingHandler>>
@ -111,6 +148,16 @@ namespace Ocelot.UnitTests.Requester
_factory.Setup(x => x.Get(It.IsAny<DownstreamReRoute>())).Returns(new OkResponse<List<Func<DelegatingHandler>>>(handlers)); _factory.Setup(x => x.Get(It.IsAny<DownstreamReRoute>())).Returns(new OkResponse<List<Func<DelegatingHandler>>>(handlers));
} }
private void GivenTheHouseReturnsTimeoutHandler()
{
var handlers = new List<Func<DelegatingHandler>>
{
() => new TimeoutDelegatingHandler()
};
_factory.Setup(x => x.Get(It.IsAny<DownstreamReRoute>())).Returns(new OkResponse<List<Func<DelegatingHandler>>>(handlers));
}
class OkDelegatingHandler : DelegatingHandler class OkDelegatingHandler : DelegatingHandler
{ {
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
@ -118,5 +165,14 @@ namespace Ocelot.UnitTests.Requester
return Task.FromResult(new HttpResponseMessage()); return Task.FromResult(new HttpResponseMessage());
} }
} }
class TimeoutDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await Task.Delay(100000, cancellationToken);
return new HttpResponseMessage();
}
}
} }
} }