diff --git a/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpHandlerResourceV3Provider.cs b/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpHandlerResourceV3Provider.cs index e1b3105c3f9..e425e515d2a 100644 --- a/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpHandlerResourceV3Provider.cs +++ b/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpHandlerResourceV3Provider.cs @@ -48,6 +48,13 @@ private static HttpHandlerResourceV3 CreateResource(PackageSource packageSource) AutomaticDecompression = (DecompressionMethods.GZip | DecompressionMethods.Deflate) }; +#if IS_DESKTOP + if (packageSource.MaxHttpRequestsPerSource > 0) + { + clientHandler.MaxConnectionsPerServer = packageSource.MaxHttpRequestsPerSource; + } +#endif + // Setup http client handler client certificates if (packageSource.ClientCertificates != null) { diff --git a/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpSourceResourceProvider.cs b/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpSourceResourceProvider.cs index 506a8f86220..597a22f026d 100644 --- a/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpSourceResourceProvider.cs +++ b/src/NuGet.Core/NuGet.Protocol/HttpSource/HttpSourceResourceProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Net; using System.Threading; using System.Threading.Tasks; using NuGet.Configuration; @@ -13,6 +14,9 @@ namespace NuGet.Protocol { public class HttpSourceResourceProvider : ResourceProvider { +#if IS_DESKTOP + private const int DefaultMaxHttpRequestsPerSource = 64; +#endif // Only one HttpSource per source should exist. This is to reduce the number of TCP connections. private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); @@ -47,6 +51,12 @@ public override Task> TryCreate(SourceRepository sou { throttle = SemaphoreSlimThrottle.CreateSemaphoreThrottle(source.PackageSource.MaxHttpRequestsPerSource); } +#if IS_DESKTOP + else if (ServicePointManager.DefaultConnectionLimit == ServicePointManager.DefaultPersistentConnectionLimit) + { + source.PackageSource.MaxHttpRequestsPerSource = DefaultMaxHttpRequestsPerSource; + } +#endif curResource = _cache.GetOrAdd( source.PackageSource, diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/HttpHandlerResourceV3ProviderTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/HttpHandlerResourceV3ProviderTests.cs new file mode 100644 index 00000000000..417c9a19674 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/HttpHandlerResourceV3ProviderTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; +using NuGet.Test.Utility; +using Xunit; + +namespace NuGet.Protocol.Tests +{ + public class HttpHandlerResourceV3ProviderTests + { + private readonly string _testPackageSourceURL = "https://contoso.test/v3/index.json"; + +#if IS_DESKTOP + [PlatformFact(Platform.Windows)] + public async Task DefaultMaxHttpRequestsPerSourceIsForwardedToV3HttpClientHandler_SuccessAsync() + { + // Arrange + var packageSource = new PackageSource(_testPackageSourceURL); + var sourceRepository = new SourceRepository(packageSource, new List() { new HttpSourceResourceProvider(), new HttpHandlerResourceV3Provider() }); + + // HttpSourceResourceProvider updates PackageSource.MaxHttpRequestsPerSource value for .NET Framework code paths + // HttpSource constructor accepts a delegate that creates HttpHandlerResource and it stores the delegate in a private variable. + // Hence used discard to ignore the return value and fetched HttpHandlerResource from the source repository to verify behavior. + _ = await sourceRepository.GetResourceAsync(); + + // Act + HttpHandlerResource httpHandlerResource = await sourceRepository.GetResourceAsync(); + + // Assert + Assert.NotNull(httpHandlerResource); + Assert.Equal(64, httpHandlerResource.ClientHandler.MaxConnectionsPerServer); + } + + [PlatformTheory(Platform.Windows)] + [InlineData(128)] + [InlineData(256)] + public async Task PackageSourceMaxHttpRequestsPerSourceIsForwardedToV3HttpClientHandler_SuccessAsync(int maxHttpRequestsPerSource) + { + // Arrange + var packageSource = new PackageSource(_testPackageSourceURL) { MaxHttpRequestsPerSource = maxHttpRequestsPerSource }; + var sourceRepository = new SourceRepository(packageSource, new List() { new HttpSourceResourceProvider(), new HttpHandlerResourceV3Provider() }); + + // HttpSourceResourceProvider updates PackageSource.MaxHttpRequestsPerSource value for .NET Framework code paths + // HttpSource constructor accepts a delegate that creates HttpHandlerResource and it stores the delegate in a private variable. + // Hence used discard to ignore the return value and fetched HttpHandlerResource from the source repository to verify behavior. + _ = await sourceRepository.GetResourceAsync(); + + // Act + HttpHandlerResource httpHandlerResource = await sourceRepository.GetResourceAsync(); + + // Assert + Assert.NotNull(httpHandlerResource); + Assert.Equal(maxHttpRequestsPerSource, httpHandlerResource.ClientHandler.MaxConnectionsPerServer); + } +#elif IS_CORECLR + + [Theory] + [InlineData(64)] + [InlineData(128)] + [InlineData(2)] + public async Task PackageSourceMaxHttpRequestsPerSourceIsNotForwardedToV3HttpClientHandler_SuccessAsync(int maxHttpRequestsPerSource) + { + // Arrange + var packageSource = new PackageSource(_testPackageSourceURL) { MaxHttpRequestsPerSource = maxHttpRequestsPerSource }; + var sourceRepository = new SourceRepository(packageSource, new[] { new HttpHandlerResourceV3Provider() }); + + // HttpSourceResourceProvider updates PackageSource.MaxHttpRequestsPerSource value for .NET Framework code paths + // HttpSource constructor accepts a delegate that creates HttpHandlerResource and it stores the delegate in a private variable. + // Hence used discard to ignore the return value and fetched HttpHandlerResource from the source repository to verify behavior. + _ = await sourceRepository.GetResourceAsync(); + + // Act + HttpHandlerResource httpHandlerResource = await sourceRepository.GetResourceAsync(); + + // Assert + Assert.NotNull(httpHandlerResource); + Assert.NotEqual(maxHttpRequestsPerSource, httpHandlerResource.ClientHandler.MaxConnectionsPerServer); + } +#endif + } +} diff --git a/test/NuGet.Core.Tests/NuGet.Protocol.Tests/HttpSourceResourceProviderTests.cs b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/HttpSourceResourceProviderTests.cs new file mode 100644 index 00000000000..de9959ddb81 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.Protocol.Tests/HttpSourceResourceProviderTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; +using NuGet.Test.Utility; +using Xunit; + +namespace NuGet.Protocol.Tests +{ + public class HttpSourceResourceProviderTests + { + private readonly string _testPackageSourceURL = "https://contoso.test/v3/index.json"; + +#if IS_DESKTOP + [PlatformFact(Platform.Windows)] + public async Task WhenMaxHttpRequestsPerSourceIsNotConfiguredThenItsValueIsSetToDefault_SuccessAsync() + { + // Arrange + var packageSource = new PackageSource(_testPackageSourceURL); + var sourceRepository = new SourceRepository(packageSource, new[] { new HttpSourceResourceProvider() }); + + // Act + HttpSourceResource httpSourceResource = await sourceRepository.GetResourceAsync(); + + // Assert + Assert.NotNull(httpSourceResource); + Assert.Equal(64, sourceRepository.PackageSource.MaxHttpRequestsPerSource); + } +#elif IS_CORECLR + [Fact] + public async Task WhenMaxHttpRequestsPerSourceIsNotConfiguredThenItsValueWillNotBeUpdated_SuccessAsync() + { + // Arrange + var packageSource = new PackageSource(_testPackageSourceURL); + var sourceRepository = new SourceRepository(packageSource, new[] { new HttpSourceResourceProvider() }); + + // Act + HttpSourceResource httpSourceResource = await sourceRepository.GetResourceAsync(); + + // Assert + Assert.NotNull(httpSourceResource); + Assert.Equal(0, sourceRepository.PackageSource.MaxHttpRequestsPerSource); + } +#endif + + [PlatformTheory] + [InlineData(128)] + [InlineData(256)] + public async Task WhenMaxHttpRequestsPerSourceIsConfiguredThenItsValueWillNotBeUpdated_SuccessAsync(int maxHttpRequestsPerSource) + { + // Arrange + var packageSource = new PackageSource(_testPackageSourceURL) { MaxHttpRequestsPerSource = maxHttpRequestsPerSource }; + var sourceRepository = new SourceRepository(packageSource, new[] { new HttpSourceResourceProvider() }); + + // Act + HttpSourceResource httpSourceResource = await sourceRepository.GetResourceAsync(); + + // Assert + Assert.NotNull(httpSourceResource); + Assert.Equal(maxHttpRequestsPerSource, sourceRepository.PackageSource.MaxHttpRequestsPerSource); + } + } +}