#222 ConnectionLeaseTimeout

This commit is contained in:
Todd Menier 2017-09-30 12:37:25 -05:00
parent 7965c12b7f
commit 49a67ecf4b
5 changed files with 104 additions and 30 deletions

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
@ -243,6 +244,23 @@ namespace Flurl.Test.Http
Assert.AreEqual("999", cli.Cookies["z"].Value);
}
[Test]
public async Task connection_lease_timeout_doesnt_disrupt_calls() {
// Specific behavior associated with ConnectionLeaseTimeout is coverd in SettingsTests.
// Here let's just make sure it isn't disruptive in any way in real calls.
var cli = new FlurlClient("http://www.google.com");
cli.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(20);
// initiate a call to google every 10ms for 100ms.
var tasks = new List<Task>();
for (var i = 0; i < 10; i++) {
tasks.Add(cli.Request().GetAsync());
await Task.Delay(10);
}
await Task.WhenAll(tasks); // failed HTTP status, etc, would throw here and fail the test.
}
public class DelegatingHandlerHttpClientFactory : DefaultHttpClientFactory
{
public override HttpMessageHandler CreateMessageHandler() {

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Flurl.Http;
using Flurl.Http.Configuration;
@ -224,6 +223,39 @@ namespace Flurl.Test.Http
Assert.IsInstanceOf<SomeCustomHttpClient>(client.HttpClient);
Assert.IsInstanceOf<SomeCustomMessageHandler>(client.HttpMessageHandler);
}
[Test]
public async Task connection_lease_timeout_sets_connection_close_header() {
using (var test = new HttpTest()) {
var client = new FlurlClient("http://api.com");
client.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(50);
await client.Request("1").GetAsync();
test.ShouldHaveCalled("http://api.com/1").WithoutHeader("Connection");
// exceed the timeout
await Task.Delay(51);
// slam it many times concurrently
await Task.WhenAll(
client.Request("2").GetAsync(),
client.Request("2").GetAsync(),
client.Request("2").GetAsync(),
client.Request("2").GetAsync(),
client.Request("2").GetAsync(),
client.Request("2").GetAsync(),
client.Request("2").GetAsync(),
client.Request("2").GetAsync());
// connection:close header should get sent exactly once
test.ShouldHaveCalled("http://api.com/2").WithHeader("Connection", "close").Times(1);
await Task.Delay(10);
await client.Request("3").GetAsync();
test.ShouldHaveCalled("http://api.com/3").WithoutHeader("Connection");
}
}
}
[TestFixture, Parallelizable]

View File

@ -172,6 +172,16 @@ namespace Flurl.Http.Configuration
/// </summary>
public ClientFlurlHttpSettings(FlurlHttpSettings defaults) : base(defaults) { }
/// <summary>
/// Specifies the time to keep the underlying HTTP/TCP conneciton open. When expired, a Connection: close header
/// is sent with the next request, which should force a new connection and DSN lookup to occur on the next call.
/// Default is null, effectively disabling the behavior.
/// </summary>
public TimeSpan? ConnectionLeaseTimeout {
get => Get(() => ConnectionLeaseTimeout);
set => Set(() => ConnectionLeaseTimeout, value);
}
/// <summary>
/// Gets or sets a factory used to create the HttpClient and HttpMessageHandler used for HTTP calls.
/// Whenever possible, custom factory implementations should inherit from DefaultHttpClientFactory,

View File

@ -41,6 +41,12 @@ namespace Flurl.Http
/// <returns>A new IFlurlRequest</returns>
IFlurlRequest Request(params object[] urlSegments);
/// <summary>
/// Checks whether the connection lease timeout (as specified in Settings.ConnectionLeaseTimeout) has passed since
/// connection was opened. If it has, resets the interval and returns true.
/// </summary>
bool CheckAndRenewConnectionLease();
/// <summary>
/// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed.
/// </summary>
@ -63,46 +69,31 @@ namespace Flurl.Http
BaseUrl = baseUrl;
Settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings);
_httpClient = new Lazy<HttpClient>(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler));
_httpMessageHandler = new Lazy<HttpMessageHandler>(() => Settings.HttpClientFactory.CreateMessageHandler());
_httpMessageHandler = new Lazy<HttpMessageHandler>(() => {
_connectionLeaseStart = DateTime.UtcNow;
return Settings.HttpClientFactory.CreateMessageHandler();
});
}
/// <summary>
/// The base URL associated with this client.
/// </summary>
/// <inheritdoc />
public string BaseUrl { get; set; }
/// <summary>
/// Gets or sets the FlurlHttpSettings object used by this client.
/// </summary>
/// <inheritdoc />
public ClientFlurlHttpSettings Settings { get; set; }
/// <summary>
/// Collection of headers sent on all requests using this client.
/// </summary>
/// <inheritdoc />
public IDictionary<string, object> Headers { get; } = new Dictionary<string, object>();
/// <summary>
/// Collection of HttpCookies sent and received on all requests using this client.
/// </summary>
/// <inheritdoc />
public IDictionary<string, Cookie> Cookies { get; } = new Dictionary<string, Cookie>();
/// <summary>
/// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated
/// to FlurlHttp.FlurlClientFactory. Reused for the life of the FlurlClient.
/// </summary>
/// <inheritdoc />
public HttpClient HttpClient => _httpClient.Value;
/// <summary>
/// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated
/// to FlurlHttp.FlurlClientFactory.
/// </summary>
/// <inheritdoc />
public HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value;
/// <summary>
/// Instantiates a new IFlurClient, optionally appending path segments to the BaseUrl.
/// </summary>
/// <param name="urlSegments">The URL or URL segments for the request. If BaseUrl is defined, it is assumed that these are path segments off that base.</param>
/// <returns>A new IFlurlRequest</returns>
/// <inheritdoc />
public IFlurlRequest Request(params object[] urlSegments) {
var parts = new List<string>(urlSegments.Select(s => s.ToInvariantString()));
if (!Url.IsValid(parts.FirstOrDefault()) && !string.IsNullOrEmpty(BaseUrl))
@ -121,9 +112,29 @@ namespace Flurl.Http
set => Settings = value as ClientFlurlHttpSettings;
}
/// <summary>
/// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed.
/// </summary>
private DateTime? _connectionLeaseStart = null;
private readonly object _connectionLeaseLock = new object();
private bool IsConnectionLeaseExpired =>
_connectionLeaseStart.HasValue &&
Settings.ConnectionLeaseTimeout.HasValue &&
DateTime.UtcNow - _connectionLeaseStart > Settings.ConnectionLeaseTimeout;
/// <inheritdoc />
public bool CheckAndRenewConnectionLease() {
// do double-check locking to avoid lock overhead most of the time
if (IsConnectionLeaseExpired) {
lock (_connectionLeaseLock) {
if (IsConnectionLeaseExpired) {
_connectionLeaseStart = DateTime.UtcNow;
return true;
}
}
}
return false;
}
/// <inheritdoc />
public bool IsDisposed { get; private set; }
/// <summary>

View File

@ -121,6 +121,9 @@ namespace Flurl.Http
if (Settings.CookiesEnabled)
WriteRequestCookies(request);
if (Client.CheckAndRenewConnectionLease())
request.Headers.ConnectionClose = true;
call.Response = await Client.HttpClient.SendAsync(request, completionOption, token).ConfigureAwait(false);
call.Response.RequestMessage = request;