#222 ConnectionLeaseTimeout
This commit is contained in:
parent
7965c12b7f
commit
49a67ecf4b
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user