From fa5d10e9f4aa603c9dfea8a8e54f0260a996b7a3 Mon Sep 17 00:00:00 2001 From: Gabriel Bider <1554615+silkfire@users.noreply.github.com> Date: Sun, 13 Dec 2020 02:59:19 +0100 Subject: [PATCH] Use concurrent dictionary to cache visited addresses in HttpHandlerEx message handler --- .../BasicClientBuilderTests.cs | 80 +++++++++++++++++-- .../MessageHandlers/HttpClientHandlerEx.cs | 26 +++--- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/Simple.HttpClientFactory.Tests/BasicClientBuilderTests.cs b/Simple.HttpClientFactory.Tests/BasicClientBuilderTests.cs index b731011..7082c9e 100644 --- a/Simple.HttpClientFactory.Tests/BasicClientBuilderTests.cs +++ b/Simple.HttpClientFactory.Tests/BasicClientBuilderTests.cs @@ -18,18 +18,27 @@ namespace Simple.HttpClientFactory.Tests public sealed class BasicClientBuilderTests : IDisposable { private const string _endpointUri = "/hello/world"; + private const string _endpointUri2 = "/hello/world2"; private readonly WireMockServer _server; public BasicClientBuilderTests() { _server = WireMockServer.Start(); + _server.Given(Request.Create().WithPath(_endpointUri).UsingAnyMethod()) .RespondWith( Response.Create() - .WithStatusCode(200) + .WithStatusCode(HttpStatusCode.OK) .WithHeader("Content-Type", "text/plain") .WithBody("Hello world!")); + + _server.Given(Request.Create().WithPath(_endpointUri2).UsingAnyMethod()) + .RespondWith( + Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello world 2!")); } @@ -141,17 +150,76 @@ public async Task Will_send_default_headers() #if NET472 [Fact] - public async Task HttpClient_will_cache_visited_urls() + public async Task HttpClientHandlerEx_should_cache_visited_url() + { + var baseUri = _server.Urls[0]; + + var clientHandler = new HttpClientHandlerEx(); + var client = HttpClientFactory.Create().Build(clientHandler); + + _ = await client.GetAsync($"{baseUri}{_endpointUri}"); + + var cachedUriKey = Assert.Single(clientHandler.AlreadySeenAddresses); + Assert.Equal(new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri}"), cachedUriKey); + } + + [Fact] + public async Task UriCacheKey_equal_comparison() + { + var baseUri = _server.Urls[0]; + + var clientHandler = new HttpClientHandlerEx(); + var client = HttpClientFactory.Create().Build(clientHandler); + + _ = await client.GetAsync($"{baseUri}{_endpointUri}"); + + var cachedUriKey = Assert.Single(clientHandler.AlreadySeenAddresses); + Assert.True(new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri}") == cachedUriKey); + } + + [Fact] + public async Task UriCacheKey_not_equal_comparison() { var baseUri = _server.Urls[0]; var clientHandler = new HttpClientHandlerEx(); - var client = HttpClientFactory.Create(baseUri).Build(clientHandler); + var client = HttpClientFactory.Create().Build(clientHandler); - _ = await client.GetAsync(_endpointUri); + _ = await client.GetAsync($"{baseUri}{_endpointUri}"); + _ = await client.GetAsync($"{baseUri}{_endpointUri2}"); - Assert.Single(clientHandler.AlreadySeenAddresses); - Assert.True(clientHandler.AlreadySeenAddresses.First() == new HttpClientHandlerEx.UriCacheKey(new Uri($"{baseUri}{_endpointUri}"))); + Assert.Equal(2, clientHandler.AlreadySeenAddresses.Count); + + var cachedUriKey = Assert.Single(clientHandler.AlreadySeenAddresses, uck => uck != new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri}")); + Assert.Equal(new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri2}"), cachedUriKey); + } + + [Fact] + public void UriCacheKey_equal_comparison_object() + { + object uriCacheKeyObject = new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}"); + + Assert.True(new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}").Equals(uriCacheKeyObject)); + } + + [Fact] + public void UriCacheKey_not_equal_comparison_object() + { + object notUriCacheKeyObject = 0; + + Assert.False(new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri2}").Equals(notUriCacheKeyObject)); + } + + [Fact] + public void UriCacheKey_ToString() + { + Assert.Equal($"{_server.Urls[0]}{_endpointUri}", new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}").ToString()); + } + + [Fact] + public void Two_UriCacheKeys_created_with_a_string_vs_a_uri_should_be_equal() + { + Assert.Equal(new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}"), new HttpClientHandlerEx.UriCacheKey(new Uri($"{_server.Urls[0]}{_endpointUri}"))); } #endif diff --git a/Simple.HttpClientFactory/MessageHandlers/HttpClientHandlerEx.cs b/Simple.HttpClientFactory/MessageHandlers/HttpClientHandlerEx.cs index a4df89c..bf653d1 100644 --- a/Simple.HttpClientFactory/MessageHandlers/HttpClientHandlerEx.cs +++ b/Simple.HttpClientFactory/MessageHandlers/HttpClientHandlerEx.cs @@ -1,6 +1,8 @@ #if NETSTANDARD2_0 using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -12,9 +14,9 @@ namespace Simple.HttpClientFactory.MessageHandlers //note: Easy.Common is licensed with MIT License (https://github.com/NimaAra/Easy.Common/blob/master/LICENSE) public class HttpClientHandlerEx : HttpClientHandler { - private readonly HashSet _alreadySeenAddresses = new HashSet(); + private readonly ConcurrentDictionary _alreadySeenAddresses = new ConcurrentDictionary(); - public IReadOnlyCollection AlreadySeenAddresses => _alreadySeenAddresses; + public IReadOnlyCollection AlreadySeenAddresses => _alreadySeenAddresses.Values.ToList().AsReadOnly(); protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { @@ -30,23 +32,19 @@ protected override Task SendAsync(HttpRequestMessage reques private void EnsureConnectionLeaseTimeout(Uri endpoint) { - if (!endpoint.IsAbsoluteUri) { return; } - var key = new UriCacheKey(endpoint); - lock (_alreadySeenAddresses) - { - if (_alreadySeenAddresses.Contains(key)) { return; } - ServicePointManager.FindServicePoint(endpoint) - .ConnectionLeaseTimeout = (int)Constants.ConnectionLifeTime.TotalMilliseconds; - _alreadySeenAddresses.Add(key); - } + _alreadySeenAddresses.TryAdd(key, key); + + ServicePointManager.FindServicePoint(endpoint).ConnectionLeaseTimeout = (int)Constants.ConnectionLifeTime.TotalMilliseconds; } - public struct UriCacheKey : IEquatable + public readonly struct UriCacheKey : IEquatable { private readonly Uri _uri; + public UriCacheKey(string uri) => _uri = new Uri(uri); + public UriCacheKey(Uri uri) => _uri = uri; public bool Equals(UriCacheKey other) => _uri == other._uri; @@ -58,8 +56,10 @@ public struct UriCacheKey : IEquatable public static bool operator ==(UriCacheKey left, UriCacheKey right) => left.Equals(right); public static bool operator !=(UriCacheKey left, UriCacheKey right) => !left.Equals(right); + + + public override string ToString() => _uri.ToString(); } } - } #endif