diff --git a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs index ef82aca225..717b68597a 100644 --- a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs +++ b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs @@ -447,6 +447,14 @@ public TimeSpan? IdleTcpConnectionTimeout /// /// /// When the time elapses, the attempt is cancelled and an error is returned. Longer timeouts will delay retries and failures. + /// The supplied is preserved unchanged on this property. At the transport boundary, + /// values in [, 1 second) are treated as 0 (use ) + /// and values greater than or equal to 1 second are rounded up to the nearest whole second + /// (for example, 2.3 seconds becomes 3 seconds). + /// Negative values are not recommended and will emit a warning trace. They are preserved + /// on this property for backward compatibility; at the transport boundary they are truncated + /// to whole seconds and the TransportClient.Options.OpenTimeout getter returns + /// for any stored value that is not greater than . /// public TimeSpan? OpenTcpConnectionTimeout { diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 38c8997dc0..c25001cc54 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -12,7 +12,8 @@ namespace Microsoft.Azure.Cosmos using System.Net; using System.Net.Http; using System.Net.Security; - using System.Security.Cryptography.X509Certificates; + using System.Security.Cryptography.X509Certificates; + using Microsoft.Azure.Cosmos.Core.Trace; using Microsoft.Azure.Cosmos.FaultInjection; using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Azure.Documents; @@ -605,12 +606,40 @@ public TimeSpan? IdleTcpConnectionTimeout /// /// /// When the time elapses, the attempt is cancelled and an error is returned. Longer timeouts will delay retries and failures. + /// + /// The supplied is preserved unchanged on this property. At the + /// transport boundary the value is converted to whole seconds: + /// + /// + /// + /// Values in [, 1 second) are treated as 0, causing the + /// configured to be used as the open-connection timeout. + /// + /// + /// Values greater than or equal to 1 second are rounded up to the nearest whole second + /// (for example, 2.3 seconds becomes 3 seconds). + /// + /// + /// Negative values are not recommended and will emit a warning trace. They are preserved + /// on this property for backward compatibility. At the transport boundary they are converted + /// to whole seconds via truncation (e.g. −5.7 s → −5) and ultimately reach the + /// TransportClient.Options.OpenTimeout getter, which returns + /// for any value that is not greater than + /// . /// public TimeSpan? OpenTcpConnectionTimeout { get => this.openTcpConnectionTimeout; set { + if (value.HasValue && value.Value < TimeSpan.Zero) + { + DefaultTrace.TraceWarning( + "OpenTcpConnectionTimeout value {0} is negative. Negative values are not recommended; " + + "the TransportClient will fall back to the configured RequestTimeout.", + value.Value); + } + this.openTcpConnectionTimeout = value; this.ValidateDirectTCPSettings(); } diff --git a/Microsoft.Azure.Cosmos/src/DocumentClient.cs b/Microsoft.Azure.Cosmos/src/DocumentClient.cs index c70b0b8668..4f439eec2a 100644 --- a/Microsoft.Azure.Cosmos/src/DocumentClient.cs +++ b/Microsoft.Azure.Cosmos/src/DocumentClient.cs @@ -927,7 +927,24 @@ internal virtual void Initialize(Uri serviceEndpoint, if (connectionPolicy.OpenTcpConnectionTimeout.HasValue) { - this.openConnectionTimeoutInSeconds = (int)connectionPolicy.OpenTcpConnectionTimeout.Value.TotalSeconds; + // Values in [TimeSpan.Zero, 1 second) become 0 (use RequestTimeout). + // Values >= 1 second round up to the nearest whole second, clamped to int.MaxValue. + // Negative values are truncated via (int)TotalSeconds, preserving pre-PR behavior. + TimeSpan openTcpConnectionTimeout = connectionPolicy.OpenTcpConnectionTimeout.Value; + + if (openTcpConnectionTimeout < TimeSpan.Zero) + { + this.openConnectionTimeoutInSeconds = (int)openTcpConnectionTimeout.TotalSeconds; + } + else if (openTcpConnectionTimeout < TimeSpan.FromSeconds(1)) + { + this.openConnectionTimeoutInSeconds = 0; + } + else + { + double ceilingSeconds = Math.Ceiling(openTcpConnectionTimeout.TotalSeconds); + this.openConnectionTimeoutInSeconds = ceilingSeconds > int.MaxValue ? int.MaxValue : (int)ceilingSeconds; + } } if (connectionPolicy.MaxRequestsPerTcpConnection.HasValue) diff --git a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs index d7a9dac0c4..a3a93d5b14 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs @@ -435,6 +435,12 @@ public CosmosClientBuilder WithConnectionModeDirect() /// Controls the amount of time allowed for trying to establish a connection. /// The default timeout is 5 seconds. Recommended values are greater than or equal to 5 seconds. /// When the time elapses, the attempt is cancelled and an error is returned. Longer timeouts will delay retries and failures. + /// At the transport boundary, values in [, 1 second) are treated as 0 + /// (use the configured request timeout). Values greater than or equal to 1 second are rounded up to + /// the nearest whole second (for example, 2.3 seconds becomes 3 seconds). + /// Negative values are not recommended and will emit a warning trace. They are preserved for + /// backward compatibility; at the transport boundary they are truncated to whole seconds and the + /// TransportClient returns the configured request timeout for any stored value not greater than zero. /// /// /// Controls the number of requests allowed simultaneously over a single TCP connection. When more requests are in flight simultaneously, the direct/TCP client will open additional connections. 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 9b3797ba11..27e7989653 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -921,6 +921,191 @@ public void VerifyGetConnectionPolicyThrowIfDirectTcpSettingAreUsedInGatewayMode Assert.ThrowsException(() => cosmosClientOptions.MaxTcpConnectionsPerEndpoint = maxTcpConnectionsPerEndpoint); } + [TestMethod] + public void OpenTcpConnectionTimeoutNegativeTimeSpanPassesThroughWithWarning() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + }; + + // Negative values are passed through unchanged (warning is logged but value is preserved). + options.OpenTcpConnectionTimeout = TimeSpan.FromMilliseconds(-1); + Assert.AreEqual(TimeSpan.FromMilliseconds(-1), options.OpenTcpConnectionTimeout, + "Negative value should be preserved unchanged on the property."); + + options.OpenTcpConnectionTimeout = TimeSpan.FromSeconds(-30); + Assert.AreEqual(TimeSpan.FromSeconds(-30), options.OpenTcpConnectionTimeout, + "Negative value should be preserved unchanged on the property."); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutZeroIsAllowedAndRoundTripsThroughConnectionPolicy() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = TimeSpan.Zero, + }; + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + Assert.AreEqual(TimeSpan.Zero, policy.OpenTcpConnectionTimeout); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutSubSecondNormalizesToZeroInRntbdConfig() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = TimeSpan.FromMilliseconds(500), + }; + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + CosmosClientBuilder builder = new CosmosClientBuilder( + accountEndpoint: AccountEndpoint, + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); + CosmosClient cosmosClient = builder.Build(new MockDocumentClient(connectionPolicy: policy)); + + Microsoft.Azure.Cosmos.Tracing.TraceData.RntbdConnectionConfig tcpConfig = + cosmosClient.ClientConfigurationTraceDatum.RntbdConnectionConfig; + + Assert.AreEqual( + 0, + tcpConfig.ConnectionTimeout, + "Sub-second OpenTcpConnectionTimeout must surface as 0 seconds (fall back to request timeout)."); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutExactlyOneSecondPreservedInRntbdConfig() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = TimeSpan.FromSeconds(1), + }; + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + CosmosClientBuilder builder = new CosmosClientBuilder( + accountEndpoint: AccountEndpoint, + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); + CosmosClient cosmosClient = builder.Build(new MockDocumentClient(connectionPolicy: policy)); + + Microsoft.Azure.Cosmos.Tracing.TraceData.RntbdConnectionConfig tcpConfig = + cosmosClient.ClientConfigurationTraceDatum.RntbdConnectionConfig; + + Assert.AreEqual(1, tcpConfig.ConnectionTimeout); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutWholeSecondsPreservedInRntbdConfig() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = TimeSpan.FromSeconds(7), + }; + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + CosmosClientBuilder builder = new CosmosClientBuilder( + accountEndpoint: AccountEndpoint, + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); + CosmosClient cosmosClient = builder.Build(new MockDocumentClient(connectionPolicy: policy)); + + Microsoft.Azure.Cosmos.Tracing.TraceData.RntbdConnectionConfig tcpConfig = + cosmosClient.ClientConfigurationTraceDatum.RntbdConnectionConfig; + + Assert.AreEqual(7, tcpConfig.ConnectionTimeout); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutFractionalRoundsUpInRntbdConfig() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = TimeSpan.FromSeconds(2.5), + }; + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + CosmosClientBuilder builder = new CosmosClientBuilder( + accountEndpoint: AccountEndpoint, + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); + CosmosClient cosmosClient = builder.Build(new MockDocumentClient(connectionPolicy: policy)); + + Microsoft.Azure.Cosmos.Tracing.TraceData.RntbdConnectionConfig tcpConfig = + cosmosClient.ClientConfigurationTraceDatum.RntbdConnectionConfig; + + Assert.AreEqual( + 3, + tcpConfig.ConnectionTimeout, + "Fractional OpenTcpConnectionTimeout (>= 1s) rounds up to the nearest whole second at the transport boundary."); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutJustOverOneSecondRoundsUpInRntbdConfig() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = TimeSpan.FromMilliseconds(1001), + }; + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + CosmosClientBuilder builder = new CosmosClientBuilder( + accountEndpoint: AccountEndpoint, + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); + CosmosClient cosmosClient = builder.Build(new MockDocumentClient(connectionPolicy: policy)); + + Microsoft.Azure.Cosmos.Tracing.TraceData.RntbdConnectionConfig tcpConfig = + cosmosClient.ClientConfigurationTraceDatum.RntbdConnectionConfig; + + Assert.AreEqual( + 2, + tcpConfig.ConnectionTimeout, + "1.001s rounds up to 2s at the transport boundary."); + } + + [TestMethod] + public void OpenTcpConnectionTimeoutFractionalPreservedOnConnectionPolicyTimeSpan() + { + TimeSpan customerSupplied = TimeSpan.FromSeconds(2.5); + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + OpenTcpConnectionTimeout = customerSupplied, + }; + + Assert.AreEqual( + customerSupplied, + options.OpenTcpConnectionTimeout, + "CosmosClientOptions preserves the supplied TimeSpan unchanged."); + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + Assert.AreEqual( + customerSupplied, + policy.OpenTcpConnectionTimeout, + "ConnectionPolicy preserves the supplied TimeSpan unchanged."); + } + + [TestMethod] + public void WithConnectionModeDirectNegativeOpenTcpTimeoutPassesThroughUnchanged() + { + CosmosClientOptions options = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Direct, + }; + + // Negative values are preserved on the property and round-trip through ConnectionPolicy. + options.OpenTcpConnectionTimeout = TimeSpan.FromSeconds(-1); + Assert.AreEqual(TimeSpan.FromSeconds(-1), options.OpenTcpConnectionTimeout, + "Negative openTcpConnectionTimeout should be preserved unchanged."); + + ConnectionPolicy policy = options.GetConnectionPolicy(clientId: 0); + Assert.AreEqual(TimeSpan.FromSeconds(-1), policy.OpenTcpConnectionTimeout, + "Negative value should round-trip through ConnectionPolicy unchanged."); + } + [TestMethod] public void VerifyHttpClientFactoryBlockedWithConnectionLimit() { diff --git a/changelog.md b/changelog.md index db34364e9c..d75ee5165a 100644 --- a/changelog.md +++ b/changelog.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Bugs Fixed +- [5873](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/5873) Direct: Fixes silent truncation of `OpenTcpConnectionTimeout`. Sub-second values in [0, 1s) are now explicitly normalized to 0 and fractional values ≥ 1 second are rounded up to the nearest whole second (for example, 2.3s becomes 3s). Negative values emit a warning trace but are left unchanged for backward compatibility. - [5783](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/5783) Container: Fixes SemanticRerankAsync TypeLoadException in derived classes ### [3.60.0](https://www.nuget.org/packages/Microsoft.Azure.Cosmos/3.60.0) - 2026-5-18