Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable Tcp settings to CosmosClientOptions #100

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 137 additions & 3 deletions Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public class CosmosClientOptions
private CosmosSerializationOptions serializerOptions;
private CosmosSerializer serializer;

private ConnectionMode connectionMode;
private Protocol connectionProtocol;
private TimeSpan? idleTcpConnectionTimeout;
private TimeSpan? openTcpConnectionTimeout;
private int? maxRequestsPerTcpConnection;
private int? maxTcpConnectionsPerEndpoint;

/// <summary>
/// Creates a new CosmosClientOptions
/// </summary>
Expand Down Expand Up @@ -157,7 +164,15 @@ public int GatewayModeMaxConnectionLimit
/// </remarks>
/// <seealso cref="CosmosClientBuilder.WithConnectionModeDirect"/>
/// <seealso cref="CosmosClientBuilder.WithConnectionModeGateway(int?)"/>
public ConnectionMode ConnectionMode { get; set; }
public ConnectionMode ConnectionMode
{
get => this.connectionMode;
set
{
this.ValidateDirectTCPSettings();
this.connectionMode = value;
}
}

/// <summary>
/// This can be used to weaken the database account consistency level for read operations.
Expand All @@ -181,7 +196,84 @@ public int GatewayModeMaxConnectionLimit
public TimeSpan? MaxRetryWaitTimeOnRateLimitedRequests { get; set; }

/// <summary>
/// Get to set optional serializer options.
/// (Direct/TCP) Controls the amount of idle time after which unused connections are closed.
/// </summary>
/// <value>
/// By default, idle connections are kept open indefinitely. Value must be greater than or equal to 10 minutes. Recommended values are between 20 minutes and 24 hours.
/// </value>
/// <remarks>
/// Mainly useful for sparse infrequent access to a large database account.
/// </remarks>
public TimeSpan? IdleTcpConnectionTimeout
{
get => this.idleTcpConnectionTimeout;
set
{
this.idleTcpConnectionTimeout = value;
this.ValidateDirectTCPSettings();
}
}

/// <summary>
/// (Direct/TCP) Controls the amount of time allowed for trying to establish a connection.
/// </summary>
/// <value>
/// The default timeout is 5 seconds. Recommended values are greater than or equal to 5 seconds.
/// </value>
/// <remarks>
/// When the time elapses, the attempt is cancelled and an error is returned. Longer timeouts will delay retries and failures.
/// </remarks>
public TimeSpan? OpenTcpConnectionTimeout
{
get => this.openTcpConnectionTimeout;
set
{
this.openTcpConnectionTimeout = value;
this.ValidateDirectTCPSettings();
}
}

/// <summary>
/// (Direct/TCP) 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.
/// </summary>
/// <value>
/// The default settings allow 30 simultaneous requests per connection.
/// Do not set this value lower than 4 requests per connection or higher than 50-100 requests per connection.
/// The former can lead to a large number of connections to be created.
/// The latter can lead to head of line blocking, high latency and timeouts.
/// </value>
/// <remarks>
/// Applications with a very high degree of parallelism per connection, with large requests or responses, or with very tight latency requirements might get better performance with 8-16 requests per connection.
/// </remarks>
public int? MaxRequestsPerTcpConnection
{
get => this.maxRequestsPerTcpConnection;
set
{
this.maxRequestsPerTcpConnection = value;
this.ValidateDirectTCPSettings();
}
}

/// <summary>
/// (Direct/TCP) Controls the maximum number of TCP connections that may be opened to each Cosmos DB back-end.
/// Together with MaxRequestsPerTcpConnection, this setting limits the number of requests that are simultaneously sent to a single Cosmos DB back-end(MaxRequestsPerTcpConnection x MaxTcpConnectionPerEndpoint).
/// </summary>
/// <value>
/// The default value is 65,535. Value must be greater than or equal to 16.
/// </value>
public int? MaxTcpConnectionsPerEndpoint
{
get => this.maxTcpConnectionsPerEndpoint;
set
{
this.maxTcpConnectionsPerEndpoint = value;
this.ValidateDirectTCPSettings();
}
}

/// <summary>
/// Get to set optional serializer options.
/// </summary>
/// <example>
/// An example on how to configure the serialization option to ignore null values
Expand Down Expand Up @@ -264,7 +356,15 @@ public CosmosSerializer Serializer
/// Gateway mode only supports HTTPS.
/// For more information, see <see href="https://docs.microsoft.com/azure/documentdb/documentdb-performance-tips#use-tcp">Connection policy: Use the TCP protocol</see>.
/// </remarks>
internal Protocol ConnectionProtocol { get; set; }
internal Protocol ConnectionProtocol
{
get => this.connectionProtocol;
set
{
this.ValidateDirectTCPSettings();
this.connectionProtocol = value;
}
}

internal UserAgentContainer UserAgentContainer { get; private set; }

Expand Down Expand Up @@ -375,6 +475,7 @@ internal CosmosClientOptions Clone()

internal ConnectionPolicy GetConnectionPolicy()
{
this.ValidateDirectTCPSettings();
ConnectionPolicy connectionPolicy = new ConnectionPolicy()
{
MaxConnectionLimit = this.GatewayModeMaxConnectionLimit,
Expand All @@ -383,6 +484,10 @@ internal ConnectionPolicy GetConnectionPolicy()
ConnectionProtocol = this.ConnectionProtocol,
UserAgentContainer = this.UserAgentContainer,
UseMultipleWriteLocations = true,
IdleTcpConnectionTimeout = this.IdleTcpConnectionTimeout,
OpenTcpConnectionTimeout = this.OpenTcpConnectionTimeout,
MaxRequestsPerTcpConnection = this.MaxRequestsPerTcpConnection,
MaxTcpConnectionsPerEndpoint = this.MaxTcpConnectionsPerEndpoint
};

if (this.ApplicationRegion != null)
Expand Down Expand Up @@ -481,6 +586,35 @@ private static string GetValueFromConnectionString(string connectionString, stri
throw new ArgumentException("The connection string is missing a required property: " + keyName);
}

private void ValidateDirectTCPSettings()
{
string settingName = string.Empty;
if (!(this.ConnectionMode == ConnectionMode.Direct && this.ConnectionProtocol == Protocol.Tcp))
{
if (this.IdleTcpConnectionTimeout.HasValue)
{
settingName = nameof(this.IdleTcpConnectionTimeout);
}
else if (this.OpenTcpConnectionTimeout.HasValue)
{
settingName = nameof(this.OpenTcpConnectionTimeout);
}
else if (this.MaxRequestsPerTcpConnection.HasValue)
{
settingName = nameof(this.MaxRequestsPerTcpConnection);
}
else if (this.MaxTcpConnectionsPerEndpoint.HasValue)
{
settingName = nameof(this.MaxTcpConnectionsPerEndpoint);
}
}

if (!string.IsNullOrEmpty(settingName))
{
throw new ArgumentException($"{settingName} requires {nameof(this.ConnectionMode)} to be set to {nameof(ConnectionMode.Direct)} and {nameof(this.ConnectionProtocol)} to be set to {nameof(Protocol.Tcp)}");
}
}

/// <summary>
/// Serialize the current configuration into a JSON string
/// </summary>
Expand Down
34 changes: 33 additions & 1 deletion Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,47 @@ public CosmosClientBuilder WithRequestTimeout(TimeSpan requestTimeout)
/// <summary>
/// Sets the connection mode to Direct. This is used by the client when connecting to the Azure Cosmos DB service.
/// </summary>
/// <param name="idleTcpConnectionTimeout">
/// Controls the amount of idle time after which unused connections are closed.
/// By default, idle connections are kept open indefinitely. Value must be greater than or equal to 10 minutes. Recommended values are between 20 minutes and 24 hours.
/// Mainly useful for sparse infrequent access to a large database account.
/// </param>
/// <param name="openTcpConnectionTimeout">
/// 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.
/// </param>
/// <param name="maxRequestsPerTcpConnection">
/// 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.
/// The default settings allow 30 simultaneous requests per connection.
/// Do not set this value lower than 4 requests per connection or higher than 50-100 requests per connection.
/// The former can lead to a large number of connections to be created.
/// The latter can lead to head of line blocking, high latency and timeouts.
/// Applications with a very high degree of parallelism per connection, with large requests or responses, or with very tight latency requirements might get better performance with 8-16 requests per connection.
/// </param>
/// <param name="maxTcpConnectionsPerEndpoint">
/// Controls the maximum number of TCP connections that may be opened to each Cosmos DB back-end.
/// Together with MaxRequestsPerTcpConnection, this setting limits the number of requests that are simultaneously sent to a single Cosmos DB back-end(MaxRequestsPerTcpConnection x MaxTcpConnectionPerEndpoint).
/// The default value is 65,535. Value must be greater than or equal to 16.
/// </param>
/// <remarks>
/// For more information, see <see href="https://docs.microsoft.com/azure/documentdb/documentdb-performance-tips#direct-connection">Connection policy: Use direct connection mode</see>.
/// </remarks>
/// <returns>The current <see cref="CosmosClientBuilder"/>.</returns>
/// <seealso cref="CosmosClientOptions.ConnectionMode"/>
public CosmosClientBuilder WithConnectionModeDirect()
public CosmosClientBuilder WithConnectionModeDirect(TimeSpan? idleTcpConnectionTimeout = null,
TimeSpan? openTcpConnectionTimeout = null,
int? maxRequestsPerTcpConnection = null,
int? maxTcpConnectionsPerEndpoint = null)
{
this.clientOptions.IdleTcpConnectionTimeout = idleTcpConnectionTimeout;
this.clientOptions.OpenTcpConnectionTimeout = openTcpConnectionTimeout;
this.clientOptions.MaxRequestsPerTcpConnection = maxRequestsPerTcpConnection;
this.clientOptions.MaxTcpConnectionsPerEndpoint = maxTcpConnectionsPerEndpoint;

this.clientOptions.ConnectionMode = ConnectionMode.Direct;
this.clientOptions.ConnectionProtocol = Protocol.Tcp;

return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated()
IgnoreNullValues = true,
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase,
};
TimeSpan idleTcpConnectionTimeout = new TimeSpan(0, 10, 0);
TimeSpan openTcpConnectionTimeout = new TimeSpan(0, 0, 5);
int maxRequestsPerTcpConnection = 30;
int maxTcpConnectionsPerEndpoint = 65535;

CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(
accountEndpoint: endpoint,
Expand Down Expand Up @@ -66,6 +70,10 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated()
Assert.AreEqual(Protocol.Tcp, policy.ConnectionProtocol);
Assert.AreEqual(clientOptions.GatewayModeMaxConnectionLimit, policy.MaxConnectionLimit);
Assert.AreEqual(clientOptions.RequestTimeout, policy.RequestTimeout);
Assert.IsNull(policy.IdleTcpConnectionTimeout);
Assert.IsNull(policy.OpenTcpConnectionTimeout);
Assert.IsNull(policy.MaxRequestsPerTcpConnection);
Assert.IsNull(policy.MaxTcpConnectionsPerEndpoint);

cosmosClientBuilder.WithApplicationRegion(region)
.WithConnectionModeGateway(maxConnections)
Expand Down Expand Up @@ -104,6 +112,33 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated()
Assert.IsTrue(policy.UseMultipleWriteLocations);
Assert.AreEqual(maxRetryAttemptsOnThrottledRequests, policy.RetryOptions.MaxRetryAttemptsOnThrottledRequests);
Assert.AreEqual((int)maxRetryWaitTime.TotalSeconds, policy.RetryOptions.MaxRetryWaitTimeInSeconds);

//Verify Direct Mode settings
cosmosClientBuilder = new CosmosClientBuilder(
accountEndpoint: endpoint,
accountKey: key);
cosmosClientBuilder.WithConnectionModeDirect(
idleTcpConnectionTimeout,
openTcpConnectionTimeout,
maxRequestsPerTcpConnection,
maxTcpConnectionsPerEndpoint
);

cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient());
clientOptions = cosmosClient.ClientOptions;

//Verify all the values are updated
Assert.AreEqual(idleTcpConnectionTimeout, clientOptions.IdleTcpConnectionTimeout);
Assert.AreEqual(openTcpConnectionTimeout, clientOptions.OpenTcpConnectionTimeout);
Assert.AreEqual(maxRequestsPerTcpConnection, clientOptions.MaxRequestsPerTcpConnection);
Assert.AreEqual(maxTcpConnectionsPerEndpoint, clientOptions.MaxTcpConnectionsPerEndpoint);

//Verify GetConnectionPolicy returns the correct values
policy = clientOptions.GetConnectionPolicy();
Assert.AreEqual(idleTcpConnectionTimeout, policy.IdleTcpConnectionTimeout);
Assert.AreEqual(openTcpConnectionTimeout, policy.OpenTcpConnectionTimeout);
Assert.AreEqual(maxRequestsPerTcpConnection, policy.MaxRequestsPerTcpConnection);
Assert.AreEqual(maxTcpConnectionsPerEndpoint, policy.MaxTcpConnectionsPerEndpoint);
}

[TestMethod]
Expand Down Expand Up @@ -264,5 +299,27 @@ public void AssertJsonSerializer()
Assert.IsInstanceOfType(cosmosClientCustom.ClientOptions.GetCosmosSerializerWithWrapperOrDefault(), typeof(CosmosJsonSerializerWrapper));
Assert.AreEqual(mockJsonSerializer, ((CosmosJsonSerializerWrapper)cosmosClientCustom.ClientOptions.GetCosmosSerializerWithWrapperOrDefault()).InternalJsonSerializer);
}

[TestMethod]
public void VerifyGetConnectionPolicyThrowIfDirectTcpSettingAreUsedInGatewayMode()
{
string endpoint = AccountEndpoint;
string key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

TimeSpan idleTcpConnectionTimeout = new TimeSpan(0, 10, 0);
TimeSpan openTcpConnectionTimeout = new TimeSpan(0, 0, 5);
int maxRequestsPerTcpConnection = 30;
int maxTcpConnectionsPerEndpoint = 65535;

CosmosClientOptions cosmosClientOptions = new CosmosClientOptions()
{
ConnectionMode = ConnectionMode.Gateway
};

Assert.ThrowsException<ArgumentException>(() => { cosmosClientOptions.IdleTcpConnectionTimeout = idleTcpConnectionTimeout; });
Assert.ThrowsException<ArgumentException>(() => { cosmosClientOptions.OpenTcpConnectionTimeout = openTcpConnectionTimeout; });
Assert.ThrowsException<ArgumentException>(() => { cosmosClientOptions.MaxRequestsPerTcpConnection = maxRequestsPerTcpConnection; });
Assert.ThrowsException<ArgumentException>(() => { cosmosClientOptions.MaxTcpConnectionsPerEndpoint = maxTcpConnectionsPerEndpoint; });
}
}
}