diff --git a/CHANGELOG.md b/CHANGELOG.md index a275e1dc..674c6f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 2024-11-08 2.4.1 +- Improved how api requests are made (utilizing IHttpClientFactory) + ## 2024-10-22 2.4.0 - CustomBet ICalculation and ICalculationFilter extended with new property Harmonization (extended with ICalculationV1 and ICalculationFilterV1) - CustomBet ISelection extended with new property Odds (extended with ISelectionV1) diff --git a/README.md b/README.md index 2c1fe9b8..30dc8bd9 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,8 @@ The SDK uses the following 3rd party libraries which must be added via the NuGet - RabbitMQ.Client (6.5.0) The package contains: -- DemoProject: A Visual Studio 2022 solution containing a demo project showing the basic usage of the SDK +- DemoProject: A solution containing a demo project showing the basic usage of the SDK - libs: DLL file composing the Odds Feed SDK -- Odds Feed SDK Documentation.chm: A documentation file describing exposed entities - Resources containing the log4net configuration needed by the Odds Feed SDK For more information please contact support@sportradar.com or visit https://iodocs.betradar.com/unifiedsdk/index.html diff --git a/docs/docs/toc.pdf b/docs/docs/toc.pdf index 45614412..52072bbf 100644 Binary files a/docs/docs/toc.pdf and b/docs/docs/toc.pdf differ diff --git a/docs/sdkapi/toc.pdf b/docs/sdkapi/toc.pdf index 706d4ea6..4182def3 100644 Binary files a/docs/sdkapi/toc.pdf and b/docs/sdkapi/toc.pdf differ diff --git a/docs/toc.pdf b/docs/toc.pdf index 8025de54..b3a4701a 100644 Binary files a/docs/toc.pdf and b/docs/toc.pdf differ diff --git a/src/Sportradar.OddsFeed.SDK.DemoProject/Sportradar.OddsFeed.SDK.DemoProject.csproj b/src/Sportradar.OddsFeed.SDK.DemoProject/Sportradar.OddsFeed.SDK.DemoProject.csproj index 0b65350c..2d1b435b 100644 --- a/src/Sportradar.OddsFeed.SDK.DemoProject/Sportradar.OddsFeed.SDK.DemoProject.csproj +++ b/src/Sportradar.OddsFeed.SDK.DemoProject/Sportradar.OddsFeed.SDK.DemoProject.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Sportradar.OddsFeed.SDK.Tests/API/Caching/VariantMarketDescriptionListCacheTests.cs b/src/Sportradar.OddsFeed.SDK.Tests/API/Caching/VariantMarketDescriptionListCacheTests.cs index 3a2849b7..9ad7726d 100644 --- a/src/Sportradar.OddsFeed.SDK.Tests/API/Caching/VariantMarketDescriptionListCacheTests.cs +++ b/src/Sportradar.OddsFeed.SDK.Tests/API/Caching/VariantMarketDescriptionListCacheTests.cs @@ -575,7 +575,7 @@ public void OnTimerWhenDataRouterManagerDisposedThenHandleDisposedException() _dataRouterManager.UriExceptions.Add(new Tuple("variant_market_descriptions.xml", new ObjectDisposedException("Drm disposed"))); _timer.FireOnce(TimeSpan.Zero); - var success = TestExecutionHelper.WaitToComplete(() => _cultures.Count == _dataRouterManager.GetCallCount(TestDataRouterManager.EndpointVariantDescriptions)); + var success = TestExecutionHelper.WaitToComplete(() => _cultures.Count == _dataRouterManager.GetCallCount(TestDataRouterManager.EndpointVariantDescriptions), timeoutMs: 20000); Assert.True(success); } @@ -845,9 +845,9 @@ public async Task LoadMarketDescriptionsWhenReloadingThenNoThrottlingHappens() // public async Task GetAllWhenNoDataPresentThenReturnEmptyCollection() // { // Assert.Equal(0, _variantsListMemoryCache.Count()); - // + // // var result = await _variantsListCache.GetAllInvariantMarketDescriptionsAsync(_cultures); - // + // // Assert.Empty(result); // } // @@ -855,9 +855,9 @@ public async Task LoadMarketDescriptionsWhenReloadingThenNoThrottlingHappens() // public async Task GetAllWhenDataPresentThenReturnAllCollection() // { // _ = await _variantsListCache.LoadMarketDescriptionsAsync(); - // + // // var result = await _variantsListCache.GetAllInvariantMarketDescriptionsAsync(_cultures); - // + // // Assert.Equal(ScheduleData.VariantListCacheCount, result.Count()); // } // diff --git a/src/Sportradar.OddsFeed.SDK.Tests/API/Config/UofSdkBootstrapApiTests.cs b/src/Sportradar.OddsFeed.SDK.Tests/API/Config/UofSdkBootstrapApiTests.cs index 02c84e93..7342f97f 100644 --- a/src/Sportradar.OddsFeed.SDK.Tests/API/Config/UofSdkBootstrapApiTests.cs +++ b/src/Sportradar.OddsFeed.SDK.Tests/API/Config/UofSdkBootstrapApiTests.cs @@ -38,9 +38,9 @@ public void HttpClientFactoryIsSingleton() [Fact] public void HttpClientIsRegistered() { - var service1 = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClient"); - var service1A = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClient"); - var service2 = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient("HttpClient"); + var service1 = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForNormal); + var service1A = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForNormal); + var service2 = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForNormal); Assert.NotNull(service1); Assert.NotNull(service1A); Assert.NotNull(service2); @@ -51,9 +51,9 @@ public void HttpClientIsRegistered() [Fact] public void HttpClientRecoveryIsRegistered() { - var service1 = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClientRecovery"); - var service1A = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClientRecovery"); - var service2 = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient("HttpClientRecovery"); + var service1 = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForRecovery); + var service1A = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForRecovery); + var service2 = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForRecovery); Assert.NotNull(service1); Assert.NotNull(service1A); Assert.NotNull(service2); @@ -64,9 +64,9 @@ public void HttpClientRecoveryIsRegistered() [Fact] public void HttpClientFastFailingIsRegistered() { - var service1 = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClientFastFailing"); - var service1A = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClientFastFailing"); - var service2 = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient("HttpClientFastFailing"); + var service1 = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForFastFailing); + var service1A = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForFastFailing); + var service2 = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForFastFailing); Assert.NotNull(service1); Assert.NotNull(service1A); Assert.NotNull(service2); @@ -77,9 +77,9 @@ public void HttpClientFastFailingIsRegistered() [Fact] public void HttpClientsHaveDifferentTimeouts() { - var httpClientNormal = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClient"); - var httpClientRecovery = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient("HttpClientRecovery"); - var httpClientFastFailing = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient("HttpClientFastFailing"); + var httpClientNormal = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForNormal); + var httpClientRecovery = ServiceScope1.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForRecovery); + var httpClientFastFailing = ServiceScope2.ServiceProvider.GetRequiredService().CreateClient(UofSdkBootstrap.HttpClientNameForFastFailing); Assert.NotNull(httpClientNormal); Assert.NotNull(httpClientRecovery); Assert.NotNull(httpClientFastFailing); @@ -93,9 +93,9 @@ public void HttpClientsHaveDifferentTimeouts() } [Fact] - public void SdkHttpClientInterfaceIsSingleton() + public void SdkHttpClientInterfaceIsTransient() { - CheckSingletonType(); + CheckTransientType(); } [Fact] @@ -168,12 +168,6 @@ public void SdkHttpClientFastFailingHasTimeoutConfigured() Assert.Equal(UofConfig.Api.HttpClientFastFailingTimeout, sdkHttpClient.Timeout); } - [Fact] - public void HttpDataFetcherIsSingleton() - { - CheckSingletonType(); - } - [Fact] public void DataFetcherIsSingleton() { diff --git a/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/HttpDataFetcherTests.cs b/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/HttpDataFetcherTests.cs index 69444df2..7eea6ea2 100644 --- a/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/HttpDataFetcherTests.cs +++ b/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/HttpDataFetcherTests.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; +using Microsoft.Extensions.DependencyInjection; using Sportradar.OddsFeed.SDK.Api.Internal.ApiAccess; using Sportradar.OddsFeed.SDK.Api.Internal.Config; using Sportradar.OddsFeed.SDK.Common.Exceptions; @@ -22,6 +23,7 @@ namespace Sportradar.OddsFeed.SDK.Tests.Entities.Rest; public class HttpDataFetcherTests { + private readonly ITestOutputHelper _outputHelper; private readonly SdkHttpClient _sdkHttpClient; private readonly HttpDataFetcher _httpDataFetcher; private readonly Uri _badUri = new Uri("http://www.unexisting-url.com"); @@ -31,11 +33,20 @@ public class HttpDataFetcherTests public HttpDataFetcherTests(ITestOutputHelper outputHelper) { - _stubMessageHandler = new StubMessageHandler(outputHelper, 100, 50); - var httpClient = new HttpClient(_stubMessageHandler); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); - _sdkHttpClient = new SdkHttpClient(httpClient); + _outputHelper = outputHelper; + const string httpClientName = "test"; + _stubMessageHandler = new StubMessageHandler(_outputHelper, 100, 50); + var services = new ServiceCollection(); + services.AddHttpClient(httpClientName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromSeconds(5); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); + }) + .ConfigurePrimaryHttpMessageHandler(() => _stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); + _sdkHttpClient = new SdkHttpClient(serviceProvider.GetRequiredService(), httpClientName); _httpDataFetcher = new HttpDataFetcher(_sdkHttpClient, new Deserializer()); } @@ -90,7 +101,7 @@ public async Task ValidateConnectionWhenConnectionFailureTime() Assert.Null(result); Assert.IsType(e); Assert.Null(e.InnerException); - Assert.Equal("Failed to execute request due to previous failures.", e.Message); + Assert.Equal("Failed to execute request due to previous failures", e.Message); } [Fact] @@ -142,6 +153,23 @@ public async Task GetDataAsyncWhenWrongUrlThenThrowsHttpRequestException() } } + [Fact] + public async Task GetDataAsyncWhenTaskIsCanceled() + { + var httpDataFetcher = SetupHttpDataFetcherForTaskTimeout(); + + Stream result = null; + var ex = await Assert.ThrowsAsync(async () => result = await httpDataFetcher.GetDataAsync(_getUri)); + + Assert.Null(result); + Assert.IsType(ex); + Assert.Equal(HttpStatusCode.RequestTimeout, ex.ResponseCode); + if (ex.InnerException != null) + { + Assert.IsType(ex.InnerException); + } + } + [Fact] public void GetDataWhenBadGatewayWithValidContent() { @@ -279,6 +307,23 @@ public async Task PostDataAsyncWhenWrongUrlThenThrowCommunicationException() Assert.Null(e.InnerException); } + [Fact] + public async Task PostDataAsyncWhenTaskIsCanceled() + { + var httpDataFetcher = SetupHttpDataFetcherForTaskTimeout(); + + HttpResponseMessage result = null; + var ex = await Assert.ThrowsAsync(async () => result = await httpDataFetcher.PostDataAsync(_getUri, new StringContent("test string"))); + + Assert.Null(result); + Assert.IsType(ex); + Assert.Equal(HttpStatusCode.RequestTimeout, ex.ResponseCode); + if (ex.InnerException != null) + { + Assert.IsType(ex.InnerException); + } + } + [Fact] public async Task PostDataAsyncWhenValidContentWithResponseHeaders() { @@ -311,6 +356,25 @@ public async Task PostDataAsyncWhenValidContentWithMultipleSameResponseHeaders() Assert.Equal("any-key", _httpDataFetcher.ResponseHeaders.First().Key); } + private HttpDataFetcher SetupHttpDataFetcherForTaskTimeout() + { + const string httpClientName = "test"; + var stubMessageHandler = new StubMessageHandler(_outputHelper, 1000); + var services = new ServiceCollection(); + services.AddHttpClient(httpClientName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromMilliseconds(200); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); + }) + .ConfigurePrimaryHttpMessageHandler(() => stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); + var sdkHttpClient = new SdkHttpClient(serviceProvider.GetRequiredService(), httpClientName); + var httpDataFetcher = new HttpDataFetcher(sdkHttpClient, new Deserializer()); + return httpDataFetcher; + } + private HttpResponseMessage GetSuccessResponseMessage(HttpStatusCode httpStatusCode = HttpStatusCode.Accepted) { var response = GetResponse(httpStatusCode); @@ -324,7 +388,7 @@ private response GetResponse(HttpStatusCode httpStatusCode = HttpStatusCode.Acce return new response { action = "some-response-action", message = $"some-response-message {httpStatusCode}", response_code = response_code.ACCEPTED, response_codeSpecified = true }; } - public static string ObjectToXmlString(T objectToSerialize) + private static string ObjectToXmlString(T objectToSerialize) { var xmlSerializer = new XmlSerializer(typeof(T)); diff --git a/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/LogHttpDataFetcherTests.cs b/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/LogHttpDataFetcherTests.cs index 82304a12..4d3ec38f 100644 --- a/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/LogHttpDataFetcherTests.cs +++ b/src/Sportradar.OddsFeed.SDK.Tests/Entities/REST/LogHttpDataFetcherTests.cs @@ -9,6 +9,8 @@ using System.Net.Http; using System.Net.Sockets; using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -28,10 +30,10 @@ namespace Sportradar.OddsFeed.SDK.Tests.Entities.Rest; public class LogHttpDataFetcherTests { private readonly ITestOutputHelper _outputHelper; + private const string HttpClientDefaultName = "test"; private readonly IncrementalSequenceGenerator _incrementalSequenceGenerator; private readonly SdkHttpClient _sdkHttpClient; private LogHttpDataFetcher _logHttpDataFetcher; - private readonly LogHttpDataFetcher _logHttpDataFetcherPool; private readonly Uri _badUri = new Uri("http://www.unexisting-url.com"); private readonly Uri _getUri = new Uri("http://test.domain.com/get"); private readonly Uri _postUri = new Uri("http://test.domain.com/post"); @@ -41,104 +43,117 @@ public LogHttpDataFetcherTests(ITestOutputHelper outputHelper) { _outputHelper = outputHelper; _stubMessageHandler = new StubMessageHandler(_outputHelper, 100, 50); - var httpClient = new HttpClient(_stubMessageHandler); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); - _sdkHttpClient = new SdkHttpClient(httpClient); - var sdkHttpClientPool = new SdkHttpClientPool("aaa", 20, TimeSpan.FromSeconds(5), _stubMessageHandler); + var services = new ServiceCollection(); + services.AddHttpClient(HttpClientDefaultName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromSeconds(5); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); + }) + .ConfigurePrimaryHttpMessageHandler(() => _stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); + _sdkHttpClient = new SdkHttpClient(serviceProvider.GetRequiredService(), HttpClientDefaultName); _incrementalSequenceGenerator = new IncrementalSequenceGenerator(new NullLogger()); _logHttpDataFetcher = new LogHttpDataFetcher(_sdkHttpClient, _incrementalSequenceGenerator, new Deserializer(), new NullLogger()); - _logHttpDataFetcherPool = new LogHttpDataFetcher(sdkHttpClientPool, _incrementalSequenceGenerator, new Deserializer(), new NullLogger()); } [Fact] - public void SdkHttpClientWithoutAccessTokenFails() + public void SdkHttpClientWhenConstructedThenSetTimeout() { - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); - - Assert.Throws(() => new SdkHttpClient(httpClient)); + _sdkHttpClient.Timeout.TotalSeconds.Should().Be(5); } [Fact] - public void SdkHttpClientWithoutUserAgentFails() + public void SdkHttpClientWhenWithoutHeadersThenThrow() { - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); + var services = new ServiceCollection(); + services.AddHttpClient(HttpClientDefaultName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromSeconds(5); + configureClient.DefaultRequestHeaders.Clear(); + }) + .ConfigurePrimaryHttpMessageHandler(() => _stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); - Assert.Throws(() => new SdkHttpClient(httpClient)); + Assert.Throws(() => new SdkHttpClient(serviceProvider.GetRequiredService(), HttpClientDefaultName)); } [Fact] - public async Task PerformanceOf100SequentialRequests() + public void SdkHttpClientWhenWithoutAccessTokenThenThrow() { - var stopwatch = Stopwatch.StartNew(); - for (var i = 0; i < 100; i++) - { - var result = await _logHttpDataFetcher.GetDataAsync(_getUri); - Assert.NotNull(result); - Assert.True(result.CanRead); - } - _outputHelper.WriteLine($"Elapsed {stopwatch.ElapsedMilliseconds} ms"); + var services = new ServiceCollection(); + services.AddHttpClient(HttpClientDefaultName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromSeconds(5); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); + }) + .ConfigurePrimaryHttpMessageHandler(() => _stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); + + Assert.Throws(() => new SdkHttpClient(serviceProvider.GetRequiredService(), HttpClientDefaultName)); } [Fact] - public async Task PerformanceOfManyParallelRequests() + public void SdkHttpClientWhenWithoutUserAgentThenThrow() { - var stopwatch = Stopwatch.StartNew(); - var tasks = new List(); - for (var i = 0; i < 1000; i++) - { - var task = _logHttpDataFetcher.GetDataAsync(GetRequestUri(false)); - tasks.Add(task); - } - await Task.WhenAll(tasks); - - Assert.True(tasks.TrueForAll(a => a.IsCompletedSuccessfully)); - _outputHelper.WriteLine($"Elapsed {stopwatch.ElapsedMilliseconds} ms"); + var services = new ServiceCollection(); + services.AddHttpClient(HttpClientDefaultName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromSeconds(5); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); + }) + .ConfigurePrimaryHttpMessageHandler(() => _stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); + + Assert.Throws(() => new SdkHttpClient(serviceProvider.GetRequiredService(), HttpClientDefaultName)); } [Fact] - public async Task PerformancePoolOfManyParallelRequests() + public void SdkHttpClientWhenWithLowCaseUserAgentThenDoNotThrow() { - var stopwatch = Stopwatch.StartNew(); - var tasks = new List(); - for (var i = 0; i < 1000; i++) - { - var task = _logHttpDataFetcherPool.GetDataAsync(GetRequestUri(false)); - tasks.Add(task); - } - await Task.WhenAll(tasks); + var services = new ServiceCollection(); + services.AddHttpClient(HttpClientDefaultName) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = TimeSpan.FromSeconds(5); + configureClient.DefaultRequestHeaders.Clear(); + configureClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); + configureClient.DefaultRequestHeaders.Add("user-agent", "UofSdk-Net"); + }) + .ConfigurePrimaryHttpMessageHandler(() => _stubMessageHandler); + var serviceProvider = services.BuildServiceProvider(); - Assert.True(tasks.TrueForAll(a => a.IsCompletedSuccessfully)); - _outputHelper.WriteLine($"Elapsed {stopwatch.ElapsedMilliseconds} ms"); + var sdkHttpClient = new SdkHttpClient(serviceProvider.GetRequiredService(), HttpClientDefaultName); + + sdkHttpClient.Should().NotBeNull(); } [Fact] - public async Task PerformanceOfManyUniqueParallelRequests() + public async Task PerformanceOf100SequentialRequests() { var stopwatch = Stopwatch.StartNew(); - var tasks = new List(); - for (var i = 0; i < 1000; i++) + for (var i = 0; i < 100; i++) { - var task = _logHttpDataFetcher.GetDataAsync(GetRequestUri(true)); - tasks.Add(task); + var result = await _logHttpDataFetcher.GetDataAsync(_getUri); + Assert.NotNull(result); + Assert.True(result.CanRead); } - await Task.WhenAll(tasks); - - Assert.True(tasks.TrueForAll(a => a.IsCompletedSuccessfully)); _outputHelper.WriteLine($"Elapsed {stopwatch.ElapsedMilliseconds} ms"); } [Fact] - public async Task PerformancePoolOfManyUniqueParallelRequests() + public async Task PerformanceOfManyParallelRequests() { var stopwatch = Stopwatch.StartNew(); var tasks = new List(); for (var i = 0; i < 1000; i++) { - var task = _logHttpDataFetcherPool.GetDataAsync(GetRequestUri(true)); + var task = _logHttpDataFetcher.GetDataAsync(GetRequestUri(false)); tasks.Add(task); } await Task.WhenAll(tasks); @@ -148,13 +163,13 @@ public async Task PerformancePoolOfManyUniqueParallelRequests() } [Fact] - public async Task PerformanceOfManyUniqueUriParallelRequests() + public async Task PerformanceOfManyUniqueParallelRequests() { var stopwatch = Stopwatch.StartNew(); var tasks = new List(); for (var i = 0; i < 1000; i++) { - var task = _logHttpDataFetcher.GetDataAsync(GetRandomUri(true)); + var task = _logHttpDataFetcher.GetDataAsync(GetRequestUri(true)); tasks.Add(task); } await Task.WhenAll(tasks); @@ -164,13 +179,13 @@ public async Task PerformanceOfManyUniqueUriParallelRequests() } [Fact] - public async Task PerformancePoolOfManyUniqueUriParallelRequests() + public async Task PerformanceOfManyUniqueUriParallelRequests() { var stopwatch = Stopwatch.StartNew(); var tasks = new List(); for (var i = 0; i < 1000; i++) { - var task = _logHttpDataFetcherPool.GetDataAsync(GetRandomUri(true)); + var task = _logHttpDataFetcher.GetDataAsync(GetRandomUri(true)); tasks.Add(task); } await Task.WhenAll(tasks); @@ -180,7 +195,7 @@ public async Task PerformancePoolOfManyUniqueUriParallelRequests() } [Fact] - public async Task GetDataAsyncWhneNormalUri() + public async Task GetDataAsyncWhenNormalUri() { // in logRest file there should be result for this call var result = await _logHttpDataFetcher.GetDataAsync(_getUri); @@ -240,6 +255,22 @@ public async Task GetDataAsyncWhenWrongUrlThenThrowsHttpRequestException() } } + [Fact] + public async Task GetDataAsyncWhenWrongUrlThenThrowsHttpRequestExceptionWithNotFound() + { + _stubMessageHandler.SetWantedResponse(new HttpRequestException("NotFound any-message", new SocketException(), HttpStatusCode.NotFound)); + Stream result = null; + + var e = await Assert.ThrowsAsync(async () => result = await _logHttpDataFetcher.GetDataAsync(_badUri)); + + Assert.Null(result); + Assert.IsType(e); + if (e.InnerException != null) + { + Assert.IsType(e.InnerException); + } + } + [Fact] public async Task GetDataAsyncWhenWrongUrlThenThrowsSocketException() { @@ -268,13 +299,9 @@ public async Task PostDataWhenNormalUri() [Fact] public async Task PostData_DebugLog() { - var httpClient = new HttpClient(_stubMessageHandler); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForAccessToken, "aaa"); - httpClient.DefaultRequestHeaders.Add(UofSdkBootstrap.HttpClientDefaultRequestHeaderForUserAgent, "UofSdk-Net"); - var sdkHttpClient = new SdkHttpClient(httpClient); var mockLocker = new Mock(); mockLocker.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true); - var logHttpDataFetcher = new LogHttpDataFetcher(sdkHttpClient, _incrementalSequenceGenerator, new Deserializer(), mockLocker.Object); + var logHttpDataFetcher = new LogHttpDataFetcher(_sdkHttpClient, _incrementalSequenceGenerator, new Deserializer(), mockLocker.Object); var result = await logHttpDataFetcher.PostDataAsync(_getUri); diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/HttpDataFetcher.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/HttpDataFetcher.cs index c2e1689b..c0c127a3 100644 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/HttpDataFetcher.cs +++ b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/HttpDataFetcher.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -85,20 +86,26 @@ public HttpDataFetcher(ISdkHttpClient sdkHttpClient, public virtual async Task GetDataAsync(Uri uri) { ValidateConnection(uri); - var responseMessage = new HttpResponseMessage(); + HttpResponseMessage responseMessage = null; try { responseMessage = await SdkHttpClient.GetAsync(uri).ConfigureAwait(false); return await ProcessGetDataAsync(responseMessage, uri).ConfigureAwait(false); } + catch (CommunicationException) + { + RecordFailure(); + throw; + } + catch (TaskCanceledException ex) + { + RecordFailure(); + throw new CommunicationException("Failed to execute http get", uri.ToString(), responseMessage?.StatusCode ?? HttpStatusCode.RequestTimeout, ex); + } catch (Exception ex) { RecordFailure(); - if (ex is CommunicationException) - { - throw; - } - throw new CommunicationException("Failed to execute http get", uri.ToString(), responseMessage.StatusCode, ex); + throw new CommunicationException("Failed to execute http get", uri.ToString(), responseMessage?.StatusCode ?? HttpStatusCode.OK, ex); } } @@ -173,7 +180,7 @@ private async Task DeserializeResponseContent(string responseContent) public virtual async Task PostDataAsync(Uri uri, HttpContent content = null) { ValidateConnection(uri); - var responseMessage = new HttpResponseMessage(); + HttpResponseMessage responseMessage = null; try { responseMessage = await SdkHttpClient.PostAsync(uri, content ?? new StringContent(string.Empty)).ConfigureAwait(false); @@ -190,14 +197,20 @@ public virtual async Task PostDataAsync(Uri uri, HttpConten } return responseMessage; } + catch (CommunicationException) + { + RecordFailure(); + throw; + } + catch (TaskCanceledException ex) + { + RecordFailure(); + throw new CommunicationException("Failed to execute http post", uri.ToString(), responseMessage?.StatusCode ?? HttpStatusCode.RequestTimeout, ex); + } catch (Exception ex) { RecordFailure(); - if (ex is CommunicationException) - { - throw; - } - throw new CommunicationException("Failed to execute http post", uri.ToString(), responseMessage.StatusCode, ex); + throw new CommunicationException("Failed to execute http post", uri.ToString(), responseMessage?.StatusCode ?? HttpStatusCode.OK, ex); } } @@ -242,7 +255,7 @@ protected void ValidateConnection(Uri uri) if (_blockingModeActive) { - throw new CommunicationException("Failed to execute request due to previous failures.", uri.ToString(), null); + throw new CommunicationException("Failed to execute request due to previous failures", uri.ToString(), null); } } } diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/LogHttpDataFetcher.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/LogHttpDataFetcher.cs index a87c8df8..5cd30164 100644 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/LogHttpDataFetcher.cs +++ b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/LogHttpDataFetcher.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Dawn; using Microsoft.Extensions.Logging; @@ -17,7 +16,7 @@ namespace Sportradar.OddsFeed.SDK.Api.Internal.ApiAccess { /// - /// A implementation of and which uses the HTTP requests to fetch or post the requested data. All request are logged. + /// An implementation of and which uses the HTTP requests to fetch or post the requested data. All request are logged. /// /// /// ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF - the levels are defined in order of increasing priority @@ -36,7 +35,7 @@ internal class LogHttpDataFetcher : HttpDataFetcher, ILogHttpDataFetcher /// Logger to log rest requests /// Indicates the limit of consecutive request failures, after which it goes in "blocking mode" /// indicates the timeout after which comes out of "blocking mode" (in seconds) - public LogHttpDataFetcher(ISdkHttpClient sdkHttpClient, ISequenceGenerator sequenceGenerator, IDeserializer responseDeserializer, ILogger logger, int connectionFailureLimit = 5, int connectionFailureTimeout = 15) + public LogHttpDataFetcher(ISdkHttpClient sdkHttpClient, ISequenceGenerator sequenceGenerator, IDeserializer responseDeserializer, ILogger logger, int connectionFailureLimit = 50, int connectionFailureTimeout = 15) : base(sdkHttpClient, responseDeserializer, connectionFailureLimit, connectionFailureTimeout) { Guard.Argument(sequenceGenerator, nameof(sequenceGenerator)).NotNull(); @@ -57,9 +56,6 @@ public override async Task GetDataAsync(Uri uri) { var dataId = _sequenceGenerator.GetNext().ToString("D7", CultureInfo.InvariantCulture); // because request can take long time, there may be several request at the same time; Id to know what belongs together. - var logBuilder = new StringBuilder(); - logBuilder.Append("Id:").Append(dataId).Append(" Fetching url: ").Append(uri.AbsoluteUri); - var watch = Stopwatch.StartNew(); Stream responseStream; try @@ -70,29 +66,27 @@ public override async Task GetDataAsync(Uri uri) catch (Exception ex) { watch.Stop(); - if (ex.GetType() == typeof(CommunicationException)) + if (!(ex is CommunicationException commException)) { - var commException = (CommunicationException)ex; - logBuilder.Append(" ResponseCode:").Append(commException.ResponseCode); - logBuilder.Append(" Duration: ").Append(watch.ElapsedMilliseconds); - logBuilder.Append(" ms Response:").Append(commException.Response?.Replace("\n", string.Empty)); - _restLog.LogError("{LogMsg}", logBuilder.ToString()); - throw; + throw new CommunicationException("Failed to execute http get", uri.ToString(), ex); } - throw new CommunicationException("Failed to execute http get", uri.ToString(), ex); + const string msgErrorTemplate = "Id:{DataId} GET: {GetUri} Response: {ResponseStatusCode} took {Elapsed} ms Response: {ResponseContent}"; + _restLog.LogError(msgErrorTemplate, dataId, uri.AbsoluteUri, commException.ResponseCode, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture), GetCommunicationExceptionContent(commException)); + throw; } - logBuilder.Append(" Duration: ").Append(watch.ElapsedMilliseconds).Append(" ms"); if (!_restLog.IsEnabled(LogLevel.Debug)) { - _restLog.LogInformation("{LogMsg}", logBuilder.ToString()); + const string msgSuccessTemplate = "Id:{DataId} GET: {GetUri} took {Elapsed} ms"; + _restLog.LogInformation(msgSuccessTemplate, dataId, uri.AbsoluteUri, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)); return responseStream; } - var responseContent = new StreamReader(responseStream).ReadToEndAsync().GetAwaiter().GetResult(); + const string msgSuccessWithContentTemplate = "Id:{DataId} GET: {GetUri} took {Elapsed} ms Response: {ResponseContent}"; + var responseContent = await new StreamReader(responseStream).ReadToEndAsync(); responseContent = responseContent.Replace("\n", string.Empty); - logBuilder.Append(" Response:").Append(responseContent); - _restLog.LogDebug("{LogMsg}", logBuilder.ToString()); + + _restLog.LogDebug(msgSuccessWithContentTemplate, dataId, uri.AbsoluteUri, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture), responseContent); var memoryStream = new MemoryStream(); var writer = new StreamWriter(memoryStream); @@ -124,6 +118,34 @@ public override async Task PostDataAsync(Uri uri, HttpConte { var dataId = _sequenceGenerator.GetNext().ToString("D7", CultureInfo.InvariantCulture); + await LogPostRequestHttpContent(dataId, uri, content); + + var watch = Stopwatch.StartNew(); + + HttpResponseMessage response = null; + try + { + response = await base.PostDataAsync(uri, content).ConfigureAwait(false); + watch.Stop(); + } + catch (Exception ex) + { + watch.Stop(); + _restLog.LogError("Id:{PostRequestId} POST: {PostUri} Response: {ResponseStatusCode} took {Elapsed} ms", dataId, uri.AbsoluteUri, response?.StatusCode, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)); + if (ex.GetType() != typeof(ObjectDisposedException) && ex.GetType() != typeof(TaskCanceledException)) + { + _restLog.LogError(ex, "{ErrorMessage}", ex.Message); + } + throw; + } + + await LogPostResponseHttpContent(dataId, uri, response, watch); + + return response; + } + + private async Task LogPostRequestHttpContent(string dataId, Uri uri, HttpContent content) + { if (content != null) { try @@ -131,11 +153,11 @@ public override async Task PostDataAsync(Uri uri, HttpConte if (_restLog.IsEnabled(LogLevel.Debug)) { var s = await content.ReadAsStringAsync().ConfigureAwait(false); - _restLog.LogDebug("Id:{PostRequestId} Posting url: {PostUri} {PostContent}", dataId, uri.AbsoluteUri, s); + _restLog.LogDebug("Id:{PostRequestId} POST url: {PostUri} {PostContent}", dataId, uri.AbsoluteUri, s); } else { - _restLog.LogInformation("Id:{PostRequestId} Posting url: {PostUri}", dataId, uri.AbsoluteUri); + _restLog.LogInformation("Id:{PostRequestId} POST url: {PostUri}", dataId, uri.AbsoluteUri); } } catch @@ -145,28 +167,12 @@ public override async Task PostDataAsync(Uri uri, HttpConte } else { - _restLog.LogInformation("Id:{PostRequestId} Posting url: {PostUri}", dataId, uri.AbsoluteUri); - } - - var watch = Stopwatch.StartNew(); - - HttpResponseMessage response; - try - { - response = await base.PostDataAsync(uri, content).ConfigureAwait(false); - } - catch (Exception ex) - { - watch.Stop(); - _restLog.LogError("Id:{PostRequestId} Posting error to {PostUri} at {Elapsed} ms", dataId, uri.AbsoluteUri, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)); - if (ex.GetType() != typeof(ObjectDisposedException) && ex.GetType() != typeof(TaskCanceledException)) - { - _restLog.LogError(ex, "{ErrorMessage}", ex.Message); - } - throw; + _restLog.LogInformation("Id:{PostRequestId} POST url: {PostUri}", dataId, uri.AbsoluteUri); } + } - watch.Stop(); + private async Task LogPostResponseHttpContent(string dataId, Uri uri, HttpResponseMessage response, Stopwatch watch) + { var responseContent = string.Empty; if (response.Content != null) { @@ -179,32 +185,40 @@ public override async Task PostDataAsync(Uri uri, HttpConte // ignored } } - if (_restLog.IsEnabled(LogLevel.Debug) || !response.IsSuccessStatusCode) + + var wantedLogLevel = SdkLoggerFactory.GetWriteLogLevel(_restLog, LogLevel.Debug); + if (!response.IsSuccessStatusCode) + { + wantedLogLevel = LogLevel.Warning; + } + else if (!_restLog.IsEnabled(LogLevel.Debug)) { - const string msgTemplate = "Id:{DataId} Posting took {Elapsed} ms. Response: {ResponseStatusCode}-{ResponseReasonPhrase} {ResponseContent}"; - if (!response.IsSuccessStatusCode) - { - _restLog.LogWarning(msgTemplate, dataId, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture), ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture), response.ReasonPhrase, responseContent); - } - else - { - _restLog.Log(SdkLoggerFactory.GetWriteLogLevel(_restLog, LogLevel.Debug), - msgTemplate, - dataId, - watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture), - ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture), - response.ReasonPhrase, - responseContent); - } + responseContent = string.Empty; } - else + const string msgTemplate = "Id:{DataId} POST: {PostUri} took {Elapsed} ms. Response: {ResponseStatusCode}-{ResponseReasonPhrase} {ResponseContent}"; + _restLog.Log(wantedLogLevel, + msgTemplate, + dataId, + uri.AbsoluteUri, + watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture), + ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture), + response.ReasonPhrase, + responseContent); + } + + private static string GetCommunicationExceptionContent(CommunicationException commException) + { + if (commException.Message.Contains("NotFound")) { - if (watch.ElapsedMilliseconds > 100) - { - _restLog.LogInformation("Id:{PostRequestId} Posting took {Elapsed} ms", dataId, watch.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture)); - } + return commException.Message; } - return response; + if (commException.InnerException != null) + { + return commException.InnerException.Message; + } + return commException.Response == null + ? commException.Message + : commException.Response.Replace("\n", string.Empty); } } } diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClient.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClient.cs index 9dadb7fd..ed9b2142 100644 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClient.cs +++ b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClient.cs @@ -10,55 +10,61 @@ namespace Sportradar.OddsFeed.SDK.Api.Internal.ApiAccess { internal class SdkHttpClient : ISdkHttpClient { - private readonly HttpClient _httpClient; - - public TimeSpan Timeout => _httpClient.Timeout; + private readonly IHttpClientFactory _httpClientFactory; + private readonly string _httpClientName; + public TimeSpan Timeout { get; } /// - public HttpRequestHeaders DefaultRequestHeaders => _httpClient.DefaultRequestHeaders; + public HttpRequestHeaders DefaultRequestHeaders { get; } - public SdkHttpClient(HttpClient httpClient) + public SdkHttpClient(IHttpClientFactory httpClientFactory, string httpClientName) { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - if (_httpClient.DefaultRequestHeaders.IsNullOrEmpty()) + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _httpClientName = string.IsNullOrEmpty(httpClientName) ? throw new ArgumentNullException(nameof(httpClientName)) : httpClientName; + + var httpClient = _httpClientFactory.CreateClient(_httpClientName); + if (httpClient.DefaultRequestHeaders.IsNullOrEmpty()) { throw new InvalidOperationException("Missing DefaultRequestHeaders"); } - if (!_httpClient.DefaultRequestHeaders.Contains("x-access-token")) + if (!httpClient.DefaultRequestHeaders.Contains("x-access-token")) { throw new InvalidOperationException("Missing x-access-token"); } - if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent")) + if (!httpClient.DefaultRequestHeaders.Contains("User-Agent")) { throw new InvalidOperationException("User-Agent"); } - //_httpClient.DefaultRequestHeaders.Add("x-access-token", accessToken); // - //_httpClient.DefaultRequestHeaders.Add("User-Agent", $"UfSdk-{SdkInfo.SdkType}/{SdkInfo.GetVersion()} (OS: {Environment.OSVersion}, NET: {Environment.Version}, Init: {SdkInfo.Created:yyyyMMddHHmm})"); - //Timeout = TimeSpan.FromSeconds(_httpClient.Timeout.TotalSeconds); + Timeout = httpClient.Timeout; + DefaultRequestHeaders = httpClient.DefaultRequestHeaders; } /// public async Task GetAsync(Uri requestUri) { - return await _httpClient.GetAsync(requestUri).ConfigureAwait(false); + var httpClient = _httpClientFactory.CreateClient(_httpClientName); + return await httpClient.GetAsync(requestUri).ConfigureAwait(false); } /// public async Task PostAsync(Uri requestUri, HttpContent content) { - return await _httpClient.PostAsync(requestUri, content).ConfigureAwait(false); + var httpClient = _httpClientFactory.CreateClient(_httpClientName); + return await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); } /// public async Task DeleteAsync(Uri requestUri) { - return await _httpClient.DeleteAsync(requestUri).ConfigureAwait(false); + var httpClient = _httpClientFactory.CreateClient(_httpClientName); + return await httpClient.DeleteAsync(requestUri).ConfigureAwait(false); } /// public async Task PutAsync(Uri requestUri, HttpContent content) { - return await _httpClient.PutAsync(requestUri, content).ConfigureAwait(false); + var httpClient = _httpClientFactory.CreateClient(_httpClientName); + return await httpClient.PutAsync(requestUri, content).ConfigureAwait(false); } } } diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientFastFailing.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientFastFailing.cs index b6d6b6ad..25483abe 100644 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientFastFailing.cs +++ b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientFastFailing.cs @@ -6,8 +6,8 @@ namespace Sportradar.OddsFeed.SDK.Api.Internal.ApiAccess { internal class SdkHttpClientFastFailing : SdkHttpClient, ISdkHttpClientFastFailing { - public SdkHttpClientFastFailing(HttpClient httpClient) - : base(httpClient) + public SdkHttpClientFastFailing(IHttpClientFactory httpClientFactory, string httpClientName) + : base(httpClientFactory, httpClientName) { } } diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientPool.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientPool.cs deleted file mode 100644 index 0449975f..00000000 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientPool.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (C) Sportradar AG.See LICENSE for full license governing this code - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Sportradar.OddsFeed.SDK.Common.Internal; -using Sportradar.OddsFeed.SDK.Common.Internal.Telemetry; - -namespace Sportradar.OddsFeed.SDK.Api.Internal.ApiAccess -{ - internal class SdkHttpClientPool : ISdkHttpClient - { - private readonly IList _httpClientPool; - private int _latest; - private readonly object _lock = new object(); - - public TimeSpan Timeout { get; } - - /// - public HttpRequestHeaders DefaultRequestHeaders { get; } - - public SdkHttpClientPool(string accessToken, int poolSize, int timeoutSecond, int maxServerConnections) - : this(accessToken, poolSize, TimeSpan.FromSeconds(timeoutSecond), maxServerConnections) - { - } - - public SdkHttpClientPool(string accessToken, int poolSize, TimeSpan timeout, int maxServerConnections) - { - if (poolSize < 1) - { - poolSize = 1; - } - var httpClientHandler = new HttpClientHandler - { - MaxConnectionsPerServer = maxServerConnections, - AllowAutoRedirect = true - }; - _httpClientPool = new List(poolSize); - for (var i = 0; i < poolSize; i++) - { - var httpClient = new HttpClient(httpClientHandler) { Timeout = timeout }; - httpClient.DefaultRequestHeaders.Add("x-access-token", accessToken); - httpClient.DefaultRequestHeaders.Add("User-Agent", $"UfSdk-{SdkInfo.SdkType}/{SdkInfo.GetVersion()} (NET: {Environment.Version}, OS: {Environment.OSVersion}, Init: {SdkInfo.Created:yyyyMMddHHmm})"); - _httpClientPool.Add(httpClient); - } - - SdkLoggerFactory.GetLoggerForExecution(typeof(SdkHttpClientPool)).LogDebug("SdkHttpClientPool with size {PoolSize} and timeout {TimeoutSeconds}s created", poolSize, timeout.TotalSeconds); - DefaultRequestHeaders = _httpClientPool.First().DefaultRequestHeaders; - Timeout = TimeSpan.FromSeconds(_httpClientPool.First().Timeout.TotalSeconds); - } - - internal SdkHttpClientPool(string accessToken, int poolSize, TimeSpan timeout, HttpMessageHandler httpMessageHandler) - { - if (poolSize < 1) - { - poolSize = 1; - } - _httpClientPool = new List(poolSize); - for (var i = 0; i < poolSize; i++) - { - var httpClient = new HttpClient(httpMessageHandler) { Timeout = timeout }; - httpClient.DefaultRequestHeaders.Add("x-access-token", accessToken); - httpClient.DefaultRequestHeaders.Add("User-Agent", $"UfSdk-{SdkInfo.SdkType}/{SdkInfo.GetVersion()} (NET: {Environment.Version}, OS: {Environment.OSVersion}, Init: {SdkInfo.Created:yyyyMMddHHmm})"); - _httpClientPool.Add(httpClient); - } - - SdkLoggerFactory.GetLoggerForExecution(typeof(SdkHttpClientPool)).LogDebug("SdkHttpClientPool with size {PoolSize} and timeout {TimeoutSeconds}s created", poolSize, timeout.TotalSeconds); - DefaultRequestHeaders = _httpClientPool.First().DefaultRequestHeaders; - } - - /// - public async Task GetAsync(Uri requestUri) - { - var httpClient = GetHttpClient(); - var result = await httpClient.GetAsync(requestUri).ConfigureAwait(false); - return result; - } - - /// - public async Task PostAsync(Uri requestUri, HttpContent content) - { - var httpClient = GetHttpClient(); - return await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); - } - - /// - public async Task DeleteAsync(Uri requestUri) - { - var httpClient = GetHttpClient(); - return await httpClient.DeleteAsync(requestUri).ConfigureAwait(false); - } - - /// - public async Task PutAsync(Uri requestUri, HttpContent content) - { - var httpClient = GetHttpClient(); - return await httpClient.PutAsync(requestUri, content).ConfigureAwait(false); - } - - private HttpClient GetHttpClient() - { - if (_httpClientPool.Count == 1) - { - return _httpClientPool[0]; - } - - lock (_lock) - { - _latest = _latest == _httpClientPool.Count - 1 ? 0 : _latest + 1; - return _httpClientPool[_latest]; - } - } - } -} diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientRecovery.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientRecovery.cs index 2bc801ff..5f323216 100644 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientRecovery.cs +++ b/src/Sportradar.OddsFeed.SDK/API/Internal/ApiAccess/SdkHttpClientRecovery.cs @@ -6,8 +6,8 @@ namespace Sportradar.OddsFeed.SDK.Api.Internal.ApiAccess { internal class SdkHttpClientRecovery : SdkHttpClient, ISdkHttpClientRecovery { - public SdkHttpClientRecovery(HttpClient httpClient) - : base(httpClient) + public SdkHttpClientRecovery(IHttpClientFactory httpClientFactory, string httpClientName) + : base(httpClientFactory, httpClientName) { } } diff --git a/src/Sportradar.OddsFeed.SDK/API/Internal/Config/UofSdkBootstrap.cs b/src/Sportradar.OddsFeed.SDK/API/Internal/Config/UofSdkBootstrap.cs index 56178a6a..6ac6544c 100644 --- a/src/Sportradar.OddsFeed.SDK/API/Internal/Config/UofSdkBootstrap.cs +++ b/src/Sportradar.OddsFeed.SDK/API/Internal/Config/UofSdkBootstrap.cs @@ -39,6 +39,9 @@ internal static class UofSdkBootstrap { internal const string HttpClientDefaultRequestHeaderForAccessToken = "x-access-token"; internal const string HttpClientDefaultRequestHeaderForUserAgent = "User-Agent"; + internal const string HttpClientNameForFastFailing = "HttpClientFastFailing"; + internal const string HttpClientNameForRecovery = "HttpClientRecovery"; + internal const string HttpClientNameForNormal = "HttpClientNormal"; internal const string CacheStoreNameForInvariantMarketDescriptionsCache = "MemoryCacheForInvariantMarketDescriptionsCache"; internal const string CacheStoreNameForVariantMarketDescriptionCache = "MemoryCacheForVariantMarketDescriptionCache"; @@ -146,61 +149,45 @@ private static void RegisterBaseTypes(IServiceCollection services, IUofConfigura private static void RegisterHttpClients(IServiceCollection services, IUofConfiguration configuration) { - var httpClientHandler = new HttpClientHandler { MaxConnectionsPerServer = configuration.Api.MaxConnectionsPerServer, AllowAutoRedirect = true }; - var userAgentData = string.Intern($"UfSdk-{SdkInfo.SdkType}/{SdkInfo.GetVersion()} (OS: {Environment.OSVersion}, NET: {Environment.Version}, Init: {DateTime.UtcNow:yyyyMMddHHmm})"); - services.AddHttpClient("HttpClient") - .ConfigureHttpClient(configureClient => - { - configureClient.Timeout = configuration.Api.HttpClientTimeout; - configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForAccessToken, configuration.AccessToken); - configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForUserAgent, userAgentData); - }) - .ConfigurePrimaryHttpMessageHandler(() => httpClientHandler); - services.AddHttpClient("HttpClientRecovery") - .ConfigureHttpClient(configureClient => - { - configureClient.Timeout = configuration.Api.HttpClientRecoveryTimeout; - configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForAccessToken, configuration.AccessToken); - configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForUserAgent, userAgentData); - }) - .ConfigurePrimaryHttpMessageHandler(() => httpClientHandler); - services.AddHttpClient("HttpClientFastFailing") - .ConfigureHttpClient(configureClient => - { - configureClient.Timeout = configuration.Api.HttpClientFastFailingTimeout; - configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForAccessToken, configuration.AccessToken); - configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForUserAgent, userAgentData); - }) - .ConfigurePrimaryHttpMessageHandler(() => httpClientHandler); - - services.AddSingleton(serviceProvider => - - new SdkHttpClient(serviceProvider.GetRequiredService().CreateClient("HttpClient")) - ); - services.AddSingleton(serviceProvider => - - new SdkHttpClientRecovery(serviceProvider.GetRequiredService().CreateClient("HttpClientRecovery")) - ); - services.AddSingleton(serviceProvider => - - new SdkHttpClientFastFailing(serviceProvider.GetRequiredService().CreateClient("HttpClientFastFailing")) - ); - - services.AddSingleton(serviceProvider => - new HttpDataFetcher( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService>()) - ); - var logHttpDataFetcherLoggerName = SdkLoggerFactory.SdkLogRepositoryName + "." + Enum.GetName(typeof(LoggerType), LoggerType.RestTraffic); + services.AddHttpClient(HttpClientNameForNormal) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = configuration.Api.HttpClientTimeout; + configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForAccessToken, configuration.AccessToken); + configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForUserAgent, userAgentData); + }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { MaxConnectionsPerServer = configuration.Api.MaxConnectionsPerServer, AllowAutoRedirect = true }); + services.AddHttpClient(HttpClientNameForRecovery) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = configuration.Api.HttpClientRecoveryTimeout; + configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForAccessToken, configuration.AccessToken); + configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForUserAgent, userAgentData); + }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { MaxConnectionsPerServer = configuration.Api.MaxConnectionsPerServer, AllowAutoRedirect = true }); + services.AddHttpClient(HttpClientNameForFastFailing) + .ConfigureHttpClient(configureClient => + { + configureClient.Timeout = configuration.Api.HttpClientFastFailingTimeout; + configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForAccessToken, configuration.AccessToken); + configureClient.DefaultRequestHeaders.Add(HttpClientDefaultRequestHeaderForUserAgent, userAgentData); + }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { MaxConnectionsPerServer = configuration.Api.MaxConnectionsPerServer, AllowAutoRedirect = true }); + + services.AddTransient(serviceProvider => new SdkHttpClient(serviceProvider.GetRequiredService(), HttpClientNameForNormal)); + services.AddTransient(serviceProvider => new SdkHttpClientRecovery(serviceProvider.GetRequiredService(), HttpClientNameForRecovery)); + services.AddTransient(serviceProvider => new SdkHttpClientFastFailing(serviceProvider.GetRequiredService(), HttpClientNameForFastFailing)); + + var restTrafficLoggerName = SdkLoggerFactory.SdkLogRepositoryName + "." + Enum.GetName(typeof(LoggerType), LoggerType.RestTraffic); services.AddSingleton(serviceProvider => new LogHttpDataFetcher( serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>(), - serviceProvider.GetService().CreateLogger(logHttpDataFetcherLoggerName)) + serviceProvider.GetService().CreateLogger(restTrafficLoggerName)) ); services.AddSingleton(serviceProvider => @@ -208,7 +195,7 @@ private static void RegisterHttpClients(IServiceCollection services, IUofConfigu serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>(), - serviceProvider.GetService().CreateLogger(logHttpDataFetcherLoggerName)) + serviceProvider.GetService().CreateLogger(restTrafficLoggerName)) ); services.AddSingleton(serviceProvider => @@ -222,7 +209,7 @@ private static void RegisterHttpClients(IServiceCollection services, IUofConfigu serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>(), - serviceProvider.GetService().CreateLogger(logHttpDataFetcherLoggerName)) + serviceProvider.GetService().CreateLogger(restTrafficLoggerName)) ); services.AddSingleton(serviceProvider => @@ -230,7 +217,7 @@ private static void RegisterHttpClients(IServiceCollection services, IUofConfigu serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>(), - serviceProvider.GetService().CreateLogger(logHttpDataFetcherLoggerName)) + serviceProvider.GetService().CreateLogger(restTrafficLoggerName)) ); services.AddSingleton(serviceProvider => @@ -238,11 +225,11 @@ private static void RegisterHttpClients(IServiceCollection services, IUofConfigu serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>(), - serviceProvider.GetService().CreateLogger(logHttpDataFetcherLoggerName)) + serviceProvider.GetService().CreateLogger(restTrafficLoggerName)) ); } - internal static void RegisterBookmakerDetailsProvider(IServiceCollection services) + private static void RegisterBookmakerDetailsProvider(IServiceCollection services) { services.AddSingleton, Deserializer>(); services.AddSingleton, BookmakerDetailsMapperFactory>(); diff --git a/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.csproj b/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.csproj index c135a6b5..99f3122f 100644 --- a/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.csproj +++ b/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.csproj @@ -14,9 +14,9 @@ https://github.com/sportradar/UnifiedOddsSdkNetCore SportRadar OddsFeed UnifiedFeed SDK NETCore NETStandard https://sdk.sportradar.com - 2.4.0.0 - 2.4.0.0 - 2.4.0 + 2.4.1.0 + 2.4.1.0 + 2.4.1 27ef2334-e9fc-4f05-b88a-a33259c908c3 True True diff --git a/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.xml b/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.xml index a221d26c..f65dc213 100644 --- a/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.xml +++ b/src/Sportradar.OddsFeed.SDK/Sportradar.OddsFeed.SDK.xml @@ -2484,7 +2484,7 @@ - A implementation of and which uses the HTTP requests to fetch or post the requested data. All request are logged. + An implementation of and which uses the HTTP requests to fetch or post the requested data. All request are logged. ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF - the levels are defined in order of increasing priority @@ -2576,21 +2576,6 @@ - - - - - - - - - - - - - - - Class used to get the bookmaker details