diff --git a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs index 6ff532db02..ec2a498ada 100644 --- a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs +++ b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs @@ -23,6 +23,8 @@ internal sealed class ConnectionPolicy private const int defaultMediaRequestTimeout = 300; private const int defaultMaxConcurrentFanoutRequests = 32; private const int defaultMaxConcurrentConnectionLimit = 50; + private const int minimumMaxRequestsPerTcpConnection = 4; + private const int minimumMaxTcpConnectionsPerEndpoint = 16; internal UserAgentContainer UserAgentContainer; private static ConnectionPolicy defaultPolicy; @@ -30,6 +32,8 @@ internal sealed class ConnectionPolicy private Protocol connectionProtocol; private ObservableCollection preferredLocations; private ObservableCollection accountInitializationCustomEndpoints; + private int? maxRequestsPerTcpConnection; + private int? maxTcpConnectionsPerEndpoint; /// /// Initializes a new instance of the class to connect to the Azure Cosmos DB service. @@ -468,8 +472,20 @@ public TimeSpan? OpenTcpConnectionTimeout /// public int? MaxRequestsPerTcpConnection { - get; - set; + get + { + return this.maxRequestsPerTcpConnection; + } + + set + { + ConnectionPolicy.ValidateDirectTcpConnectionLimit( + value, + ConnectionPolicy.minimumMaxRequestsPerTcpConnection, + nameof(this.MaxRequestsPerTcpConnection)); + + this.maxRequestsPerTcpConnection = value; + } } /// @@ -481,8 +497,20 @@ public int? MaxRequestsPerTcpConnection /// public int? MaxTcpConnectionsPerEndpoint { - get; - set; + get + { + return this.maxTcpConnectionsPerEndpoint; + } + + set + { + ConnectionPolicy.ValidateDirectTcpConnectionLimit( + value, + ConnectionPolicy.minimumMaxTcpConnectionsPerEndpoint, + nameof(this.MaxTcpConnectionsPerEndpoint)); + + this.maxTcpConnectionsPerEndpoint = value; + } } /// @@ -607,5 +635,19 @@ internal RetryWithConfiguration GetRetryWithConfiguration() { return this.RetryOptions?.GetRetryWithConfiguration(); } + + private static void ValidateDirectTcpConnectionLimit( + int? value, + int minimumValue, + string settingName) + { + if (value.HasValue && value.Value < minimumValue) + { + throw new ArgumentOutOfRangeException( + settingName, + value.Value, + $"{settingName} must be greater than or equal to {minimumValue}."); + } + } } -} \ No newline at end of file +} diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 9e3b8dfc00..0502d0f2bc 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -49,6 +49,8 @@ public class CosmosClientOptions /// Default Protocol mode /// private const Protocol DefaultProtocol = Protocol.Tcp; + private const int MinimumMaxRequestsPerTcpConnection = 4; + private const int MinimumMaxTcpConnectionsPerEndpoint = 16; private const string ConnectionStringAccountEndpoint = "AccountEndpoint"; private const string ConnectionStringAccountKey = "AccountKey"; @@ -621,6 +623,14 @@ public int? MaxRequestsPerTcpConnection get => this.maxRequestsPerTcpConnection; set { + if (this.ConnectionMode == ConnectionMode.Direct) + { + CosmosClientOptions.ValidateDirectTcpConnectionLimit( + value, + CosmosClientOptions.MinimumMaxRequestsPerTcpConnection, + nameof(this.MaxRequestsPerTcpConnection)); + } + this.maxRequestsPerTcpConnection = value; this.ValidateDirectTCPSettings(); } @@ -638,6 +648,14 @@ public int? MaxTcpConnectionsPerEndpoint get => this.maxTcpConnectionsPerEndpoint; set { + if (this.ConnectionMode == ConnectionMode.Direct) + { + CosmosClientOptions.ValidateDirectTcpConnectionLimit( + value, + CosmosClientOptions.MinimumMaxTcpConnectionsPerEndpoint, + nameof(this.MaxTcpConnectionsPerEndpoint)); + } + this.maxTcpConnectionsPerEndpoint = value; this.ValidateDirectTCPSettings(); } @@ -1329,6 +1347,29 @@ private void ValidateDirectTCPSettings() { throw new ArgumentException($"{settingName} requires {nameof(this.ConnectionMode)} to be set to {nameof(ConnectionMode.Direct)}"); } + + CosmosClientOptions.ValidateDirectTcpConnectionLimit( + this.MaxRequestsPerTcpConnection, + CosmosClientOptions.MinimumMaxRequestsPerTcpConnection, + nameof(this.MaxRequestsPerTcpConnection)); + CosmosClientOptions.ValidateDirectTcpConnectionLimit( + this.MaxTcpConnectionsPerEndpoint, + CosmosClientOptions.MinimumMaxTcpConnectionsPerEndpoint, + nameof(this.MaxTcpConnectionsPerEndpoint)); + } + + private static void ValidateDirectTcpConnectionLimit( + int? value, + int minimumValue, + string settingName) + { + if (value.HasValue && value.Value < minimumValue) + { + throw new ArgumentOutOfRangeException( + settingName, + value.Value, + $"{settingName} must be greater than or equal to {minimumValue}."); + } } internal UserAgentContainer CreateUserAgentContainerWithFeatures(int clientId) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs index 1fe9127de4..5502f41ea5 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -809,11 +809,11 @@ public void ThrowOnMissingAccountEndpointInConnectionString() } [TestMethod] - public void VerifyGetConnectionPolicyThrowIfDirectTcpSettingAreUsedInGatewayMode() - { - TimeSpan idleTcpConnectionTimeout = new TimeSpan(0, 10, 0); - TimeSpan openTcpConnectionTimeout = new TimeSpan(0, 0, 5); - int maxRequestsPerTcpConnection = 30; + public void VerifyGetConnectionPolicyThrowIfDirectTcpSettingAreUsedInGatewayMode() + { + TimeSpan idleTcpConnectionTimeout = new TimeSpan(0, 10, 0); + TimeSpan openTcpConnectionTimeout = new TimeSpan(0, 0, 5); + int maxRequestsPerTcpConnection = 30; int maxTcpConnectionsPerEndpoint = 65535; CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() @@ -824,14 +824,69 @@ public void VerifyGetConnectionPolicyThrowIfDirectTcpSettingAreUsedInGatewayMode Assert.ThrowsException(() => cosmosClientOptions.IdleTcpConnectionTimeout = idleTcpConnectionTimeout); Assert.ThrowsException(() => cosmosClientOptions.OpenTcpConnectionTimeout = openTcpConnectionTimeout); Assert.ThrowsException(() => cosmosClientOptions.MaxRequestsPerTcpConnection = maxRequestsPerTcpConnection); - Assert.ThrowsException(() => cosmosClientOptions.MaxTcpConnectionsPerEndpoint = maxTcpConnectionsPerEndpoint); - } - - [TestMethod] - public void VerifyHttpClientFactoryBlockedWithConnectionLimit() - { - CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() - { + Assert.ThrowsException(() => cosmosClientOptions.MaxTcpConnectionsPerEndpoint = maxTcpConnectionsPerEndpoint); + } + + [TestMethod] + public void VerifyCosmosClientOptionsDirectTcpConnectionLimitsValidateMinimumValues() + { + CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() + { + ConnectionMode = ConnectionMode.Direct + }; + + cosmosClientOptions.MaxTcpConnectionsPerEndpoint = null; + Assert.IsNull(cosmosClientOptions.MaxTcpConnectionsPerEndpoint); + + cosmosClientOptions.MaxTcpConnectionsPerEndpoint = 16; + Assert.AreEqual(16, cosmosClientOptions.MaxTcpConnectionsPerEndpoint); + + Assert.ThrowsException(() => cosmosClientOptions.MaxTcpConnectionsPerEndpoint = 15); + Assert.ThrowsException(() => cosmosClientOptions.MaxTcpConnectionsPerEndpoint = 0); + Assert.ThrowsException(() => cosmosClientOptions.MaxTcpConnectionsPerEndpoint = -1); + + cosmosClientOptions.MaxRequestsPerTcpConnection = null; + Assert.IsNull(cosmosClientOptions.MaxRequestsPerTcpConnection); + + cosmosClientOptions.MaxRequestsPerTcpConnection = 4; + Assert.AreEqual(4, cosmosClientOptions.MaxRequestsPerTcpConnection); + + Assert.ThrowsException(() => cosmosClientOptions.MaxRequestsPerTcpConnection = 3); + Assert.ThrowsException(() => cosmosClientOptions.MaxRequestsPerTcpConnection = 0); + Assert.ThrowsException(() => cosmosClientOptions.MaxRequestsPerTcpConnection = -1); + } + + [TestMethod] + public void VerifyConnectionPolicyDirectTcpConnectionLimitsValidateMinimumValues() + { + ConnectionPolicy connectionPolicy = new ConnectionPolicy(); + + connectionPolicy.MaxTcpConnectionsPerEndpoint = null; + Assert.IsNull(connectionPolicy.MaxTcpConnectionsPerEndpoint); + + connectionPolicy.MaxTcpConnectionsPerEndpoint = 16; + Assert.AreEqual(16, connectionPolicy.MaxTcpConnectionsPerEndpoint); + + Assert.ThrowsException(() => connectionPolicy.MaxTcpConnectionsPerEndpoint = 15); + Assert.ThrowsException(() => connectionPolicy.MaxTcpConnectionsPerEndpoint = 0); + Assert.ThrowsException(() => connectionPolicy.MaxTcpConnectionsPerEndpoint = -1); + + connectionPolicy.MaxRequestsPerTcpConnection = null; + Assert.IsNull(connectionPolicy.MaxRequestsPerTcpConnection); + + connectionPolicy.MaxRequestsPerTcpConnection = 4; + Assert.AreEqual(4, connectionPolicy.MaxRequestsPerTcpConnection); + + Assert.ThrowsException(() => connectionPolicy.MaxRequestsPerTcpConnection = 3); + Assert.ThrowsException(() => connectionPolicy.MaxRequestsPerTcpConnection = 0); + Assert.ThrowsException(() => connectionPolicy.MaxRequestsPerTcpConnection = -1); + } + + [TestMethod] + public void VerifyHttpClientFactoryBlockedWithConnectionLimit() + { + CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() + { GatewayModeMaxConnectionLimit = 42 }; @@ -1336,4 +1391,4 @@ public int Compare(object x, object y) } } } -} \ No newline at end of file +} diff --git a/changelog.md b/changelog.md index 9761217a21..df0eac5d1c 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Bugs Fixed +- Fixes Direct/TCP connection limit options to reject values below their documented minimums. See [PR 5883](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/5883). + #### Other Changes ### [3.61.0-preview.0](https://www.nuget.org/packages/Microsoft.Azure.Cosmos/3.61.0-preview.0) - 2026-5-18