diff --git a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs index 5366aca94b..cd582f0c18 100644 --- a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs +++ b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs @@ -536,6 +536,15 @@ internal CosmosClientTelemetryOptions CosmosClientTelemetryOptions set; } + /// + /// provides SessionTokenMismatchRetryPolicy optimization through customer supplied region switch hints + /// + internal SessionRetryOptions SessionRetryOptions + { + get; + set; + } + /// /// GlobalEndpointManager will subscribe to this event if user updates the preferredLocations list in the Azure Cosmos DB service. /// diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 770963efe8..7508bb1620 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -89,7 +89,8 @@ public CosmosClientOptions() this.ConnectionProtocol = CosmosClientOptions.DefaultProtocol; this.ApiType = CosmosClientOptions.DefaultApiType; this.CustomHandlers = new Collection(); - this.CosmosClientTelemetryOptions = new CosmosClientTelemetryOptions(); + this.CosmosClientTelemetryOptions = new CosmosClientTelemetryOptions(); + this.SessionRetryOptions = new SessionRetryOptions(); } /// @@ -120,7 +121,12 @@ public string ApplicationName /// /// Get or set session container for the client /// - internal ISessionContainer SessionContainer { get; set; } + internal ISessionContainer SessionContainer { get; set; } + + /// + /// hint which guide SDK-internal retry policies on how early to switch retries to a different region. + /// + internal SessionRetryOptions SessionRetryOptions { get; private set; } /// /// Gets or sets the location where the application is running. This will influence the SDK's choice for the Azure Cosmos DB service interaction. @@ -740,6 +746,20 @@ public Func HttpClientFactory /// after the threshold step time, the SDK will hedge to the third region and so on. /// public AvailabilityStrategy AvailabilityStrategy { get; set; } + + /// + /// provides SessionTokenMismatchRetryPolicy optimization through customer supplied region switch hints + /// +#if PREVIEW + public +#else + internal +#endif + bool EnableRemoteRegionPreferredForSessionRetry + { + get => this.SessionRetryOptions.RemoteRegionPreferred; + set => this.SessionRetryOptions.RemoteRegionPreferred = value; + } /// /// Enable partition key level failover @@ -1004,7 +1024,8 @@ internal virtual ConnectionPolicy GetConnectionPolicy(int clientId) ConnectionProtocol = this.ConnectionProtocol, UserAgentContainer = this.CreateUserAgentContainerWithFeatures(clientId), UseMultipleWriteLocations = true, - IdleTcpConnectionTimeout = this.IdleTcpConnectionTimeout, + IdleTcpConnectionTimeout = this.IdleTcpConnectionTimeout, + SessionRetryOptions = this.SessionRetryOptions, OpenTcpConnectionTimeout = this.OpenTcpConnectionTimeout, MaxRequestsPerTcpConnection = this.MaxRequestsPerTcpConnection, MaxTcpConnectionsPerEndpoint = this.MaxTcpConnectionsPerEndpoint, diff --git a/Microsoft.Azure.Cosmos/src/DocumentClient.cs b/Microsoft.Azure.Cosmos/src/DocumentClient.cs index 7772dcd5a1..34fa32ae10 100644 --- a/Microsoft.Azure.Cosmos/src/DocumentClient.cs +++ b/Microsoft.Azure.Cosmos/src/DocumentClient.cs @@ -6800,7 +6800,8 @@ private void CreateStoreModel(bool subscribeRntbdStatus) !this.enableRntbdChannel, this.UseMultipleWriteLocations && (this.accountServiceConfiguration.DefaultConsistencyLevel != Documents.ConsistencyLevel.Strong), true, - enableReplicaValidation: this.isReplicaAddressValidationEnabled); + enableReplicaValidation: this.isReplicaAddressValidationEnabled, + sessionRetryOptions: this.ConnectionPolicy.SessionRetryOptions); if (subscribeRntbdStatus) { diff --git a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs index 6cd332c011..0f4adf9f2d 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs @@ -581,8 +581,24 @@ public CosmosClientBuilder WithSerializerOptions(CosmosSerializationOptions cosm { this.clientOptions.SerializerOptions = cosmosSerializerOptions; return this; - } - + } + + /// + /// provides SessionTokenMismatchRetryPolicy optimization through customer supplied region switch hints + /// + /// + /// The object +#if PREVIEW + public +#else + internal +#endif + CosmosClientBuilder WithEnableRemoteRegionPreferredForSessionRetry(bool enableRemoteRegionPreferredForSessionRetry) + { + this.clientOptions.EnableRemoteRegionPreferredForSessionRetry = enableRemoteRegionPreferredForSessionRetry; + return this; + } + /// /// Set a custom JSON serializer. /// diff --git a/Microsoft.Azure.Cosmos/src/SessionRetryOptions.cs b/Microsoft.Azure.Cosmos/src/SessionRetryOptions.cs new file mode 100644 index 0000000000..e233efaa97 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/SessionRetryOptions.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using Microsoft.Azure.Documents; + + /// + /// Implementation of ISessionRetryOptions interface, do not want clients to subclass. + /// + internal sealed class SessionRetryOptions : ISessionRetryOptions + { + /// + /// Initializes a new instance of the class. + /// + public SessionRetryOptions() + { + this.MinInRegionRetryTime = ConfigurationManager.GetMinRetryTimeInLocalRegionWhenRemoteRegionPreferred(); + this.MaxInRegionRetryCount = ConfigurationManager.GetMaxRetriesInLocalRegionWhenRemoteRegionPreferred(); + } + /// + /// Sets the minimum retry time for 404/1002 retries within each region for read and write operations. + /// The minimum value is 100ms - this minimum is enforced to provide a way for the local region to catch-up on replication lag. The default value is 500ms - as a recommendation ensure that this value is higher than the steady-state + /// replication latency between the regions you chose + /// + public TimeSpan MinInRegionRetryTime { get; private set; } + + /// + /// Sets the maximum number of retries within each region for read and write operations. The minimum value is 1 - the backoff time for the last in-region retry will ensure that the total retry time within the + /// region is at least the min. in-region retry time. + /// + public int MaxInRegionRetryCount { get; private set; } + + /// + /// hints which guide SDK-internal retry policies on how early to switch retries to a different region. If true, will retry all replicas once and add a minimum delay before switching to the next region.If false, it will + /// retry in the local region up to 5s + /// + public bool RemoteRegionPreferred { get; set; } = false; + + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs b/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs index a2d0f3652d..622f81c81b 100644 --- a/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs +++ b/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs @@ -72,6 +72,20 @@ internal static class ConfigurationManager /// internal static readonly string DistributedQueryGatewayModeEnabled = "AZURE_COSMOS_DISTRIBUTED_QUERY_GATEWAY_ENABLED"; + /// + /// intent is If a client specify a value, we will force it to be atleast 100ms, otherwise default is going to be 500ms + /// + internal static readonly string MinInRegionRetryTimeForWritesInMs = "AZURE_COSMOS_SESSION_TOKEN_MISMATCH_IN_REGION_RETRY_TIME_IN_MILLISECONDS"; + internal static readonly int DefaultMinInRegionRetryTimeForWritesInMs = 500; + internal static readonly int MinMinInRegionRetryTimeForWritesInMs = 100; + + /// + /// intent is If a client specify a value, we will force it to be atleast 1, otherwise default is going to be 1(right now both the values are 1 but we have the provision to change them in future). + /// + internal static readonly string MaxRetriesInLocalRegionWhenRemoteRegionPreferred = "AZURE_COSMOS_MAX_RETRIES_IN_LOCAL_REGION_WHEN_REMOTE_REGION_PREFERRED"; + internal static readonly int DefaultMaxRetriesInLocalRegionWhenRemoteRegionPreferred = 1; + internal static readonly int MinMaxRetriesInLocalRegionWhenRemoteRegionPreferred = 1; + /// /// A read-only string containing the environment variable name for enabling binary encoding. This will eventually /// be removed once binary encoding is enabled by default for both preview @@ -96,6 +110,26 @@ public static T GetEnvironmentVariable(string variable, T defaultValue) return (T)Convert.ChangeType(value, typeof(T)); } + public static int GetMaxRetriesInLocalRegionWhenRemoteRegionPreferred() + { + return Math.Max( + ConfigurationManager + .GetEnvironmentVariable( + variable: MaxRetriesInLocalRegionWhenRemoteRegionPreferred, + defaultValue: DefaultMaxRetriesInLocalRegionWhenRemoteRegionPreferred), + MinMaxRetriesInLocalRegionWhenRemoteRegionPreferred); + } + + public static TimeSpan GetMinRetryTimeInLocalRegionWhenRemoteRegionPreferred() + { + return TimeSpan.FromMilliseconds(Math.Max( + ConfigurationManager + .GetEnvironmentVariable( + variable: MinInRegionRetryTimeForWritesInMs, + defaultValue: DefaultMinInRegionRetryTimeForWritesInMs), + MinMinInRegionRetryTimeForWritesInMs)); + } + /// /// Gets the boolean value of the replica validation environment variable. Note that, replica validation /// is enabled by default for the preview package and disabled for GA at the moment. The user can set the diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs new file mode 100644 index 0000000000..4775d99322 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs @@ -0,0 +1,416 @@ +namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Diagnostics; + using System.Linq; + using System.Net; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos; + using Microsoft.Azure.Cosmos.FaultInjection; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Container = Container; + + [TestClass] + public class SessionRetryOptionsTest + { + private string connectionString; + private IDictionary writeRegionMap; + + // to run code before running each test + [TestInitialize] + public async Task TestInitAsync() + { + this.connectionString = ConfigurationManager.GetEnvironmentVariable("COSMOSDB_MULTI_REGION", null); + if (string.IsNullOrEmpty(this.connectionString)) + { + Assert.Fail("Set environment variable COSMOSDB_MULTI_REGION to run the tests"); + } + + CosmosClient client = new CosmosClient(this.connectionString); + await MultiRegionSetupHelpers.GetOrCreateMultiRegionDatabaseAndContainers(client); + this.writeRegionMap = client.DocumentClient.GlobalEndpointManager.GetAvailableWriteEndpointsByLocation(); + Assert.IsTrue(this.writeRegionMap.Count() >= 2); + + } + [TestMethod] + [DataRow(FaultInjectionOperationType.ReadItem, 2, true, DisplayName = "Validate Read Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.QueryItem, 1, true, DisplayName = "Validate Query Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.ReadItem, 2, false, DisplayName = "Validate Read Item operation with local region preferred.")] + [DataRow(FaultInjectionOperationType.QueryItem, 2, false, DisplayName = "Validate Query Item operation with local region preferred.")] + [TestCategory("MultiMaster")] + public async Task ReadOperationWithReadSessionUnavailableTest(FaultInjectionOperationType faultInjectionOperationType, + int sessionTokenMismatchRetryAttempts, Boolean remoteRegionPreferred) + { + string[] preferredRegions = this.writeRegionMap.Keys.ToArray(); + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, "100"); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, Convert.ToString(sessionTokenMismatchRetryAttempts)); + try + { + // if I go to first region for reading an item, I should get a 404/2002 response for 10 minutes + FaultInjectionRule badSessionTokenRule = new FaultInjectionRuleBuilder( + id: "badSessionTokenRule", + condition: + new FaultInjectionConditionBuilder() + .WithOperationType(faultInjectionOperationType) + .WithRegion(preferredRegions[0]) + .Build(), + result: + FaultInjectionResultBuilder.GetResultBuilder(FaultInjectionServerErrorType.ReadSessionNotAvailable) + .Build()) + .WithDuration(TimeSpan.FromMinutes(10)) + .Build(); + + List rules = new List() { badSessionTokenRule }; + FaultInjector faultInjector = new FaultInjector(rules); + Assert.IsNotNull(faultInjector); + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + EnableRemoteRegionPreferredForSessionRetry = remoteRegionPreferred, + ConsistencyLevel = ConsistencyLevel.Session, + ApplicationPreferredRegions = preferredRegions, + ConnectionMode = ConnectionMode.Direct, + }; + + using (CosmosClient faultInjectionClient = new CosmosClient( + connectionString: this.connectionString, + clientOptions: faultInjector.GetFaultInjectionClientOptions(clientOptions))) + { + Database database = faultInjectionClient.GetDatabase(MultiRegionSetupHelpers.dbName); + Container container = await database.CreateContainerIfNotExistsAsync("sessionRetryPolicy", "/id"); + string GUID = Guid.NewGuid().ToString(); + dynamic testObject = new + { + id = GUID, + name = "customer one", + address = new + { + line1 = "45 new street", + city = "mckinney", + postalCode = "98989", + } + + }; + + ItemResponse response = await container.CreateItemAsync(testObject); + Assert.IsNotNull(response); + + OperationExecutionResult executionResult = await this.PerformDocumentOperation(faultInjectionOperationType, container, testObject); + this.ValidateOperationExecutionResult(executionResult, remoteRegionPreferred); + + // For a non-write operation, the request can go to multiple replicas (upto 4 replicas) + // Check if the SessionTokenMismatchRetryPolicy retries on the bad / lagging region + // for sessionTokenMismatchRetryAttempts by tracking the badSessionTokenRule hit count + long hitCount = badSessionTokenRule.GetHitCount(); + + if (remoteRegionPreferred) + { + Assert.IsTrue(hitCount >= sessionTokenMismatchRetryAttempts && hitCount <= (1 + sessionTokenMismatchRetryAttempts) * 4); + } + } + } + finally + { + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, null); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, null); + } + } + + [TestMethod] + [DataRow(FaultInjectionOperationType.CreateItem, 2, true, DisplayName = "Validate Write Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.ReplaceItem, 1, true, DisplayName = "Validate Replace Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.DeleteItem, 2, true, DisplayName = "Validate Delete Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.UpsertItem, 3, true, DisplayName = "Validate Upsert Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.PatchItem, 1, true, DisplayName = "Validate Patch Item operation with remote region preferred.")] + [DataRow(FaultInjectionOperationType.CreateItem, 3, false, DisplayName = "Validate Write Item operation with local region preferred.")] + [DataRow(FaultInjectionOperationType.ReplaceItem, 1, false, DisplayName = "Validate Replace Item operation with local region preferred.")] + [DataRow(FaultInjectionOperationType.DeleteItem, 2, false, DisplayName = "Validate Delete Item operation with local region preferred.")] + [DataRow(FaultInjectionOperationType.UpsertItem, 1, false, DisplayName = "Validate Upsert Item operation with local region preferred.")] + [DataRow(FaultInjectionOperationType.PatchItem, 1, false, DisplayName = "Validate Patch Item operation with remote region preferred.")] + [TestCategory("MultiMaster")] + public async Task WriteOperationWithReadSessionUnavailableTest(FaultInjectionOperationType faultInjectionOperationType, + int sessionTokenMismatchRetryAttempts, Boolean remoteRegionPreferred) + { + + string[] preferredRegions = this.writeRegionMap.Keys.ToArray(); + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, "100"); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, Convert.ToString(sessionTokenMismatchRetryAttempts)); + + try + { + FaultInjectionRule badSessionTokenRule = new FaultInjectionRuleBuilder( + id: "badSessionTokenRule", + condition: + new FaultInjectionConditionBuilder() + .WithOperationType(faultInjectionOperationType) + .WithRegion(preferredRegions[0]) + .Build(), + result: + FaultInjectionResultBuilder.GetResultBuilder(FaultInjectionServerErrorType.ReadSessionNotAvailable) + .Build()) + .WithDuration(TimeSpan.FromMinutes(10)) + .Build(); + + List rules = new List() { badSessionTokenRule }; + FaultInjector faultInjector = new FaultInjector(rules); + + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + EnableRemoteRegionPreferredForSessionRetry = remoteRegionPreferred, + ConsistencyLevel = ConsistencyLevel.Session, + ApplicationPreferredRegions = preferredRegions, + ConnectionMode = ConnectionMode.Direct, + }; + + using (CosmosClient faultInjectionClient = new CosmosClient( + connectionString: this.connectionString, + clientOptions: faultInjector.GetFaultInjectionClientOptions(clientOptions))) + { + Database database = faultInjectionClient.GetDatabase(MultiRegionSetupHelpers.dbName); + Container container = await database.CreateContainerIfNotExistsAsync("sessionRetryPolicy", "/id"); + string GUID = Guid.NewGuid().ToString(); + dynamic testObject = new + { + id = GUID, + name = "customer one", + address = new + { + line1 = "45 new street", + city = "mckinney", + postalCode = "98989", + } + + }; + + OperationExecutionResult executionResult = await this.PerformDocumentOperation(faultInjectionOperationType, container, testObject); + this.ValidateOperationExecutionResult(executionResult, remoteRegionPreferred); + + // For a write operation, the request can just go to the primary replica + // Check if the SessionTokenMismatchRetryPolicy retries on the bad / lagging region + // for sessionTokenMismatchRetryAttempts by tracking the badSessionTokenRule hit count + long hitCount = badSessionTokenRule.GetHitCount(); + if (remoteRegionPreferred) + { + // higher hit count is possible while in MinRetryWaitTimeWithinRegion + Assert.IsTrue(hitCount >= sessionTokenMismatchRetryAttempts); + } + } + } + finally + { + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, null); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, null); + } + } + + private void ValidateOperationExecutionResult(OperationExecutionResult operationExecutionResult, Boolean remoteRegionPreferred) + { + int sessionTokenMismatchDefaultWaitTime = 5000; + + FaultInjectionOperationType executionOpType = operationExecutionResult.OperationType; + HttpStatusCode statusCode = operationExecutionResult.StatusCode; + + int executionDuration = operationExecutionResult.Duration; + Trace.TraceInformation($" status code is {statusCode}"); + Trace.TraceInformation($" execution duration is {executionDuration}"); + + if (executionOpType == FaultInjectionOperationType.CreateItem) + { + Assert.IsTrue(statusCode == HttpStatusCode.Created); + } + else if (executionOpType == FaultInjectionOperationType.DeleteItem) + { + Assert.IsTrue(statusCode == HttpStatusCode.NoContent); + + } + else if (executionOpType == FaultInjectionOperationType.UpsertItem) + { + Assert.IsTrue(statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.Created); + + } + else + { + Assert.IsTrue(statusCode == HttpStatusCode.OK); + } + + if (remoteRegionPreferred) + { + Assert.IsTrue(executionDuration < sessionTokenMismatchDefaultWaitTime); + } + else + { + Assert.IsTrue(executionDuration > sessionTokenMismatchDefaultWaitTime); + } + } + + + private async Task PerformDocumentOperation(FaultInjectionOperationType operationType, Container container, + dynamic testObject) + { + + Stopwatch durationTimer = new Stopwatch(); + if (operationType == FaultInjectionOperationType.ReadItem) + { + durationTimer.Start(); + ItemResponse itemResponse = await container.ReadItemAsync(testObject.id, + new PartitionKey(testObject.id)); + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + return new OperationExecutionResult( + itemResponse.Diagnostics, + timeElapsed, + itemResponse.StatusCode, + operationType); + + } + + if (operationType == FaultInjectionOperationType.CreateItem) + { + durationTimer.Start(); + ItemResponse itemResponse = await container.CreateItemAsync(testObject); + + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + return new OperationExecutionResult( + itemResponse.Diagnostics, + timeElapsed, + itemResponse.StatusCode, + operationType); + + } + + if (operationType == FaultInjectionOperationType.ReplaceItem) + { + + await container.CreateItemAsync(testObject); + durationTimer.Start(); + + ItemResponse itemResponse = await container.ReplaceItemAsync(testObject, testObject.id, new PartitionKey(testObject.id)); + + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + return new OperationExecutionResult( + itemResponse.Diagnostics, + timeElapsed, + itemResponse.StatusCode, + operationType); + + } + + + if (operationType == FaultInjectionOperationType.UpsertItem) + { + + durationTimer.Start(); + ItemResponse itemResponse = await container.UpsertItemAsync(testObject, new PartitionKey(testObject.id)); + + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + return new OperationExecutionResult( + itemResponse.Diagnostics, + timeElapsed, + itemResponse.StatusCode, + operationType); + + } + + if (operationType == FaultInjectionOperationType.DeleteItem) + { + + await container.CreateItemAsync(testObject); + + durationTimer.Start(); + ItemResponse itemResponse = await container.DeleteItemAsync(testObject.id, new PartitionKey(testObject.id)); + + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + return new OperationExecutionResult( + itemResponse.Diagnostics, + timeElapsed, + itemResponse.StatusCode, + operationType); + + } + + if (operationType == FaultInjectionOperationType.QueryItem) + { + durationTimer.Start(); + String query = $"SELECT * from c where c.id = \"{testObject.id}\""; + FeedIterator feed = container.GetItemQueryIterator(query); + Assert.IsTrue(feed.HasMoreResults); + FeedResponse feedResponse = null; + while (feed.HasMoreResults) + { + feedResponse = await feed.ReadNextAsync(); + Assert.IsNotNull(feedResponse); + Trace.TraceInformation($" feed response count is {feedResponse.Count}"); + Assert.IsTrue(feedResponse.Count == 1); + } + + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + return new OperationExecutionResult( + feedResponse.Diagnostics, + timeElapsed, + feedResponse.StatusCode, + operationType); + + } + + if (operationType == FaultInjectionOperationType.PatchItem) + { + await container.CreateItemAsync(testObject); + durationTimer.Start(); + + ItemResponse itemResponse = await container.PatchItemAsync(testObject.id, new PartitionKey(testObject.id), + patchOperations: new[] + { + PatchOperation.Replace("/name", "Customer Two") + }); + + durationTimer.Stop(); + int timeElapsed = Convert.ToInt32(durationTimer.Elapsed.TotalMilliseconds); + + + + return new OperationExecutionResult( + itemResponse.Diagnostics, + timeElapsed, + itemResponse.StatusCode, + operationType); + + } + + + + return null; + } + + } + + internal class OperationExecutionResult + { + public CosmosDiagnostics Diagnostics { get; set; } + public int Duration { get; set; } + public HttpStatusCode StatusCode { get; set; } + public FaultInjectionOperationType OperationType { get; set; } + + public OperationExecutionResult(CosmosDiagnostics diagnostics, int duration, HttpStatusCode statusCode, FaultInjectionOperationType operationType) + { + this.Diagnostics = diagnostics; + this.Duration = duration; + this.StatusCode = statusCode; + this.OperationType = operationType; + } + } + + + +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index 268a52d9f5..926221f2f3 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -321,6 +321,16 @@ "Microsoft.Azure.Cosmos.CosmosClientOptions;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { + "Boolean EnableRemoteRegionPreferredForSessionRetry": { + "Type": "Property", + "Attributes": [], + "MethodInfo": "Boolean EnableRemoteRegionPreferredForSessionRetry;CanRead:True;CanWrite:True;Boolean get_EnableRemoteRegionPreferredForSessionRetry();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_EnableRemoteRegionPreferredForSessionRetry(Boolean);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, + "Boolean get_EnableRemoteRegionPreferredForSessionRetry()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Boolean get_EnableRemoteRegionPreferredForSessionRetry();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.Nullable`1[System.Int32] get_ThroughputBucket()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", "Attributes": [ @@ -333,6 +343,11 @@ "Attributes": [], "MethodInfo": "System.Nullable`1[System.Int32] ThroughputBucket;CanRead:True;CanWrite:True;System.Nullable`1[System.Int32] get_ThroughputBucket();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;Void set_ThroughputBucket(System.Nullable`1[System.Int32]);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "Void set_EnableRemoteRegionPreferredForSessionRetry(Boolean)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Void set_EnableRemoteRegionPreferredForSessionRetry(Boolean);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Void set_ThroughputBucket(System.Nullable`1[System.Int32])[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { "Type": "Method", "Attributes": [ @@ -1161,6 +1176,11 @@ "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder;System.Object;IsAbstract:False;IsSealed:False;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": { "Subclasses": {}, "Members": { + "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithEnableRemoteRegionPreferredForSessionRetry(Boolean)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithEnableRemoteRegionPreferredForSessionRetry(Boolean);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithThroughputBucket(Int32)": { "Type": "Method", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SessionRetryOptionsUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SessionRetryOptionsUnitTests.cs new file mode 100644 index 0000000000..a441ec0669 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SessionRetryOptionsUnitTests.cs @@ -0,0 +1,76 @@ +namespace Microsoft.Azure.Cosmos.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Tests for + /// + [TestClass] + public class SessionRetryOptionsUnitTests + { + [TestMethod] + public void SessionRetryOptionsValidValuesTest() + { + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, "200"); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, "1"); + try + { + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + EnableRemoteRegionPreferredForSessionRetry = true, + }; + + Assert.IsTrue(clientOptions.SessionRetryOptions.MinInRegionRetryTime == TimeSpan.FromMilliseconds(200)); + Assert.IsTrue(clientOptions.SessionRetryOptions.MaxInRegionRetryCount == 1); + } + finally + { + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, null); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, null); + } + + } + + [TestMethod] + public void SessionRetryOptionsDefaultValuesTest() + { + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + EnableRemoteRegionPreferredForSessionRetry = true, + }; + + Assert.IsTrue(clientOptions.SessionRetryOptions.MinInRegionRetryTime == TimeSpan.FromMilliseconds(500)); + Assert.IsTrue(clientOptions.SessionRetryOptions.MaxInRegionRetryCount == 1); + + } + + [TestMethod] + public void SessionRetryOptionsInValidValuesTest() + { + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, "50"); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, "0"); + try + { + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + EnableRemoteRegionPreferredForSessionRetry = true, + }; + + Assert.IsTrue(clientOptions.SessionRetryOptions.MinInRegionRetryTime == TimeSpan.FromMilliseconds(100)); + Assert.IsTrue(clientOptions.SessionRetryOptions.MaxInRegionRetryCount == 1); + } + finally + { + Environment.SetEnvironmentVariable(ConfigurationManager.MinInRegionRetryTimeForWritesInMs, null); + Environment.SetEnvironmentVariable(ConfigurationManager.MaxRetriesInLocalRegionWhenRemoteRegionPreferred, null); + } + + } + + } +} \ No newline at end of file