diff --git a/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs b/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs index f3141bd675..37805d5b34 100644 --- a/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs +++ b/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs @@ -509,6 +509,9 @@ private async Task ValidateAndSetConsistencyLevelAsync(RequestMessage requestMes /// /// Validate and set the ReadConsistencyStrategy header. + /// When the strategy is LastCommittedWriteRegion and the operation is a read, + /// also set the hub region processing header so the backend routes the request + /// to the hub (write) region. /// private Task ValidateAndSetReadConsistencyStrategyAsync(RequestMessage requestMessage) { @@ -529,6 +532,14 @@ private Task ValidateAndSetReadConsistencyStrategyAsync(RequestMessage requestMe requestMessage.Headers.Set( HttpConstants.HttpHeaders.ReadConsistencyStrategy, readConsistencyStrategy.Value.ToString()); + + if (readConsistencyStrategy.Value == Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion + && OperationTypeExtensions.IsReadOperation(requestMessage.OperationType)) + { + requestMessage.Headers.Set( + HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, + bool.TrueString); + } } return Task.CompletedTask; diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs index 943492369d..c81a4a3342 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs @@ -48,6 +48,12 @@ enum ReadConsistencyStrategy /// Quorum read with GCLSN barrier - returns the latest version across all regions. /// Only valid for accounts configured with Strong consistency. /// - GlobalStrong = 4 + GlobalStrong = 4, + + /// + /// Returns the latest committed version from the hub (write) region, ensuring reads + /// reflect the most recent writes regardless of which region the client is connected to. + /// + LastCommittedWriteRegion = 5 } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs index 5b01f886c3..7e0a5dd4e3 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -41,7 +41,6 @@ public class CosmosItemTests : BaseCosmosClientHelper private Container Container = null; private ContainerProperties containerSettings = null; - private const string HubRegionHeader = "x-ms-cosmos-hub-region-processing-only"; private static readonly string nonPartitionItemId = "fixed-Container-Item"; private static readonly string undefinedPartitionItemId = "undefined-partition-Item"; @@ -4340,7 +4339,7 @@ public async Task ReadItemAsync_ShouldAddHubHeader_OnRetryAfter_404_1002() // Header should NOT be present on first retry (2nd request) if (requestCount == 2 && - request.Headers.TryGetValues(HubRegionHeader, out IEnumerable firstRetryValues) && + request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable firstRetryValues) && firstRetryValues.Any()) { Assert.Fail("Header should NOT be present on first retry attempt."); @@ -4422,6 +4421,328 @@ public async Task ReadItemAsync_ShouldAddHubHeader_OnRetryAfter_404_1002() Assert.AreEqual(2, return404Count, "Both requests should have returned 404/1002"); } + /// + /// Verifies that when ReadConsistencyStrategy.LastCommittedWriteRegion is set on + /// ItemRequestOptions, the hub region header and ReadConsistencyStrategy header are + /// present from the very first request and persist through all 403/3 retries. + /// Only LastCommittedWriteRegion triggers the hub header on reads. + /// Parameterized over forbidden count to cover single and multiple 403/3 retries. + /// + [TestMethod] + [Owner("aavasthy")] + [DataRow(1, DisplayName = "LastCommittedWriteRegion request-level: single 403/3 → success")] + [DataRow(3, DisplayName = "LastCommittedWriteRegion request-level: multiple 403/3 → success")] + [Description("Request-level LastCommittedWriteRegion sets hub header on reads with 403/3 retry flow.")] + public async Task ReadItemAsync_WithLastCommittedWriteRegion_RequestLevel_403_3_ThenSuccess(int forbiddenCount) + { + int docReadRequestCount = 0; + int return403Count = 0; + List hubHeaderPerRequest = new List(); + List rcsHeaderPerRequest = new List(); + + HttpClientHandlerHelper httpHandler = new HttpClientHandlerHelper + { + RequestCallBack = (request, cancellationToken) => + { + if (request.Method == HttpMethod.Get + && request.RequestUri != null + && request.RequestUri.AbsolutePath.Contains("/docs/")) + { + docReadRequestCount++; + + bool hasHubHeader = request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable values) + && values.Any(); + hubHeaderPerRequest.Add(hasHubHeader); + + string rcsValue = null; + if (request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues)) + { + rcsValue = rcsValues.FirstOrDefault(); + } + rcsHeaderPerRequest.Add(rcsValue); + + if (return403Count < forbiddenCount) + { + return403Count++; + + HttpResponseMessage forbiddenResponse = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent( + JsonConvert.SerializeObject(new { code = "Forbidden", message = "Simulated 403/3 WriteForbidden - not hub region" }), + Encoding.UTF8, + "application/json") + }; + forbiddenResponse.Headers.Add("x-ms-substatus", ((int)SubStatusCodes.WriteForbidden).ToString()); + forbiddenResponse.Headers.Add("x-ms-activity-id", Guid.NewGuid().ToString()); + forbiddenResponse.Headers.Add("x-ms-request-charge", "1.0"); + + return Task.FromResult(forbiddenResponse); + } + } + + return Task.FromResult(null); + } + }; + + CosmosClientOptions clientOptions = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = Cosmos.ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(httpHandler), + }; + + using CosmosClient customClient = TestCommon.CreateCosmosClient(clientOptions); + Container customContainer = customClient.GetContainer(this.database.Id, this.Container.Id); + + ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); + await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); + + ItemResponse response = await customContainer.ReadItemAsync( + testItem.id, + new Cosmos.PartitionKey(testItem.pk), + new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.IsNotNull(response.Resource); + Assert.AreEqual(testItem.id, response.Resource.id); + + Assert.AreEqual(forbiddenCount, return403Count, $"Should have returned 403/3 exactly {forbiddenCount} time(s)."); + Assert.IsTrue(docReadRequestCount >= forbiddenCount + 1, + $"Expected at least {forbiddenCount + 1} requests ({forbiddenCount}x 403/3 + 1x success), got {docReadRequestCount}."); + + for (int i = 0; i < hubHeaderPerRequest.Count; i++) + { + Assert.IsTrue(hubHeaderPerRequest[i], + $"Hub region header MUST be present on request #{i + 1} for LastCommittedWriteRegion read."); + Assert.AreEqual("LastCommittedWriteRegion", rcsHeaderPerRequest[i], + $"ReadConsistencyStrategy header MUST be 'LastCommittedWriteRegion' on request #{i + 1}."); + } + } + + /// + /// Verifies that when ReadConsistencyStrategy.LastCommittedWriteRegion is set at the + /// CosmosClientOptions level, the hub region header is present on the first read request + /// and the 403/3 retry flow succeeds. + /// + [TestMethod] + [Owner("aavasthy")] + [Description("Client-level LastCommittedWriteRegion sets hub header on reads with 403/3 then success.")] + public async Task ReadItemAsync_WithLastCommittedWriteRegion_ClientLevel_403_3_ThenSuccess() + { + int docReadRequestCount = 0; + int return403Count = 0; + const int maxReturn403 = 1; + List hubHeaderPerRequest = new List(); + List rcsHeaderPerRequest = new List(); + + HttpClientHandlerHelper httpHandler = new HttpClientHandlerHelper + { + RequestCallBack = (request, cancellationToken) => + { + if (request.Method == HttpMethod.Get + && request.RequestUri != null + && request.RequestUri.AbsolutePath.Contains("/docs/")) + { + docReadRequestCount++; + + bool hasHubHeader = request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable values) + && values.Any(); + hubHeaderPerRequest.Add(hasHubHeader); + + string rcsValue = null; + if (request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues)) + { + rcsValue = rcsValues.FirstOrDefault(); + } + rcsHeaderPerRequest.Add(rcsValue); + + if (return403Count < maxReturn403) + { + return403Count++; + + HttpResponseMessage forbiddenResponse = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent( + JsonConvert.SerializeObject(new { code = "Forbidden", message = "Simulated 403/3" }), + Encoding.UTF8, + "application/json") + }; + forbiddenResponse.Headers.Add("x-ms-substatus", ((int)SubStatusCodes.WriteForbidden).ToString()); + forbiddenResponse.Headers.Add("x-ms-activity-id", Guid.NewGuid().ToString()); + forbiddenResponse.Headers.Add("x-ms-request-charge", "1.0"); + + return Task.FromResult(forbiddenResponse); + } + } + + return Task.FromResult(null); + } + }; + + CosmosClientOptions clientOptions = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = Cosmos.ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(httpHandler), + ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion + }; + + using CosmosClient customClient = TestCommon.CreateCosmosClient(clientOptions); + Container customContainer = customClient.GetContainer(this.database.Id, this.Container.Id); + + ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); + await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); + + // Read without per-request options — client-level strategy should take effect + ItemResponse response = await customContainer.ReadItemAsync( + testItem.id, + new Cosmos.PartitionKey(testItem.pk)); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(testItem.id, response.Resource.id); + Assert.AreEqual(maxReturn403, return403Count, "Should have returned 403/3 exactly once."); + Assert.IsTrue(docReadRequestCount >= 2, $"Expected at least 2 requests, got {docReadRequestCount}."); + + for (int i = 0; i < hubHeaderPerRequest.Count; i++) + { + Assert.IsTrue(hubHeaderPerRequest[i], + $"Hub region header MUST be present on request #{i + 1} for client-level LastCommittedWriteRegion."); + Assert.AreEqual("LastCommittedWriteRegion", rcsHeaderPerRequest[i], + $"ReadConsistencyStrategy header MUST be 'LastCommittedWriteRegion' on request #{i + 1}."); + } + } + + /// + /// Verifies that non-LastCommittedWriteRegion strategies (e.g. Session, Eventual) + /// set the ReadConsistencyStrategy header but do NOT set the hub region header. + /// This is the key behavioral distinction: only LastCommittedWriteRegion triggers + /// hub region routing. + /// + [TestMethod] + [Owner("aavasthy")] + [DataRow("Session", DisplayName = "Session strategy: RCS header present, no hub header")] + [DataRow("Eventual", DisplayName = "Eventual strategy: RCS header present, no hub header")] + [DataRow("LatestCommitted", DisplayName = "LatestCommitted strategy: RCS header present, no hub header")] + [Description("Non-LastCommittedWriteRegion strategies set RCS header but NOT hub region header.")] + public async Task ReadItemAsync_WithNonLastCommittedWriteRegionStrategy_NoHubHeader(string strategyName) + { + Cosmos.ReadConsistencyStrategy strategy = + (Cosmos.ReadConsistencyStrategy)Enum.Parse(typeof(Cosmos.ReadConsistencyStrategy), strategyName); + + bool hubHeaderOnFirstRequest = false; + string rcsHeaderValue = null; + bool interceptedFirstRequest = false; + + HttpClientHandlerHelper httpHandler = new HttpClientHandlerHelper + { + RequestCallBack = (request, cancellationToken) => + { + if (!interceptedFirstRequest + && request.Method == HttpMethod.Get + && request.RequestUri != null + && request.RequestUri.AbsolutePath.Contains("/docs/")) + { + interceptedFirstRequest = true; + + hubHeaderOnFirstRequest = request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable values) + && values.Any(); + + if (request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues)) + { + rcsHeaderValue = rcsValues.FirstOrDefault(); + } + } + + return Task.FromResult(null); + } + }; + + CosmosClientOptions clientOptions = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = Cosmos.ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(httpHandler), + }; + + using CosmosClient customClient = TestCommon.CreateCosmosClient(clientOptions); + Container customContainer = customClient.GetContainer(this.database.Id, this.Container.Id); + + ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); + await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); + + ItemResponse response = await customContainer.ReadItemAsync( + testItem.id, + new Cosmos.PartitionKey(testItem.pk), + new ItemRequestOptions { ReadConsistencyStrategy = strategy }); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.IsTrue(interceptedFirstRequest, "Should have intercepted at least one doc read request."); + Assert.IsFalse(hubHeaderOnFirstRequest, + $"Hub region header should NOT be present for {strategyName} strategy — only LastCommittedWriteRegion triggers it."); + Assert.AreEqual(strategyName, rcsHeaderValue, + $"ReadConsistencyStrategy header should be '{strategyName}'."); + } + + /// + /// Verifies that when ReadConsistencyStrategy is NOT set at all, neither the hub header + /// nor the ReadConsistencyStrategy header is present on the request. + /// + [TestMethod] + [Owner("aavasthy")] + [Description("Without ReadConsistencyStrategy, neither hub header nor RCS header is present.")] + public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHeaders() + { + bool hubHeaderOnFirstRequest = false; + bool rcsHeaderOnFirstRequest = false; + bool interceptedFirstRequest = false; + + HttpClientHandlerHelper httpHandler = new HttpClientHandlerHelper + { + RequestCallBack = (request, cancellationToken) => + { + if (!interceptedFirstRequest + && request.Method == HttpMethod.Get + && request.RequestUri != null + && request.RequestUri.AbsolutePath.Contains("/docs/")) + { + interceptedFirstRequest = true; + + hubHeaderOnFirstRequest = request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable values) + && values.Any(); + + rcsHeaderOnFirstRequest = request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues) + && rcsValues.Any(); + } + + return Task.FromResult(null); + } + }; + + CosmosClientOptions clientOptions = new CosmosClientOptions + { + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = Cosmos.ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(httpHandler), + }; + + using CosmosClient customClient = TestCommon.CreateCosmosClient(clientOptions); + Container customContainer = customClient.GetContainer(this.database.Id, this.Container.Id); + + ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); + await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); + + ItemResponse response = await customContainer.ReadItemAsync( + testItem.id, + new Cosmos.PartitionKey(testItem.pk)); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.IsTrue(interceptedFirstRequest, "Should have intercepted at least one doc read request."); + Assert.IsFalse(hubHeaderOnFirstRequest, + "Hub region header should NOT be present when ReadConsistencyStrategy is not set."); + Assert.IsFalse(rcsHeaderOnFirstRequest, + "ReadConsistencyStrategy header should NOT be present when ReadConsistencyStrategy is not set."); + } + private async Task AutoGenerateIdPatternTest(Cosmos.PartitionKey pk, T itemWithoutId) { string autoId = Guid.NewGuid().ToString(); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ClientRetryPolicyTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ClientRetryPolicyTests.cs index b595711070..3e0ec7cb84 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ClientRetryPolicyTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ClientRetryPolicyTests.cs @@ -28,7 +28,6 @@ public sealed class ClientRetryPolicyTests private static Uri Location1Endpoint = new Uri("https://location1.documents.azure.com"); private static Uri Location2Endpoint = new Uri("https://location2.documents.azure.com"); - private const string HubRegionHeader = "x-ms-cosmos-hub-region-processing-only"; private ReadOnlyCollection preferredLocations; private AccountProperties databaseAccount; private GlobalPartitionEndpointManager partitionKeyRangeLocationCache; @@ -434,7 +433,7 @@ public async Task ClientRetryPolicy_HubRegionHeader_AddedOn404_1002_BasedOnAccou // First attempt - header should not exist retryPolicy.OnBeforeSendRequest(request); - Assert.IsNull(request.Headers.GetValues(HubRegionHeader), "Header should not exist on initial request before any 404/1002 error."); + Assert.IsNull(request.Headers.GetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion), "Header should not exist on initial request before any 404/1002 error."); // Simulate first 404/1002 error DocumentClientException sessionNotAvailableException = new DocumentClientException( @@ -450,7 +449,7 @@ public async Task ClientRetryPolicy_HubRegionHeader_AddedOn404_1002_BasedOnAccou // First retry attempt - header should NOT be present yet retryPolicy.OnBeforeSendRequest(request); - string[] headerValues = request.Headers.GetValues(HubRegionHeader); + string[] headerValues = request.Headers.GetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion); Assert.IsNull(headerValues, "Header should NOT be present on first retry attempt (before it fails)."); // Simulate first retry also failing with 404/1002 @@ -498,7 +497,7 @@ public async Task ClientRetryPolicy_HubRegionHeader_AddedOn404_1002_BasedOnAccou { // Now verify the header is present on this retry triggered by 503 retryPolicy.OnBeforeSendRequest(request); - headerValues = request.Headers.GetValues(HubRegionHeader); + headerValues = request.Headers.GetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion); Assert.IsNotNull(headerValues, "Header should be present on retry after 404/1002 flag was set."); Assert.AreEqual(1, headerValues.Length, "Header should have exactly one value."); Assert.AreEqual(bool.TrueString, headerValues[0], "Header value should be 'True'."); @@ -512,7 +511,7 @@ public async Task ClientRetryPolicy_HubRegionHeader_AddedOn404_1002_BasedOnAccou if (shouldRetry.ShouldRetry) { retryPolicy.OnBeforeSendRequest(request); - headerValues = request.Headers.GetValues(HubRegionHeader); + headerValues = request.Headers.GetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion); Assert.IsNull(headerValues, $"Header should NOT be present on retry attempt {retryAttempt} for multi-master account."); // Simulate another 404/1002 or 503 to continue retry loop diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json index 9a78dbe18a..02f1cf4648 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.net6.json @@ -1491,6 +1491,11 @@ "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.ReadConsistencyStrategy GlobalStrong;IsInitOnly:False;IsStatic:True;" }, + "Microsoft.Azure.Cosmos.ReadConsistencyStrategy LastCommittedWriteRegion": { + "Type": "Field", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.ReadConsistencyStrategy LastCommittedWriteRegion;IsInitOnly:False;IsStatic:True;" + }, "Microsoft.Azure.Cosmos.ReadConsistencyStrategy LatestCommitted": { "Type": "Field", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs index 219260a587..5b09b9e189 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs @@ -827,6 +827,7 @@ public int Compare(object x, object y) [DataRow("Session")] [DataRow("LatestCommitted")] [DataRow("GlobalStrong")] + [DataRow("LastCommittedWriteRegion")] public async Task ReadConsistencyStrategyRequestOptionSetsHeaders(string strategyName) { Cosmos.ReadConsistencyStrategy strategy = Enum.Parse(strategyName); @@ -837,6 +838,18 @@ public async Task ReadConsistencyStrategyRequestOptionSetsHeaders(string strateg Assert.AreEqual(strategy.ToString(), request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy]); Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ConsistencyLevel], "ConsistencyLevel header should not be set when ReadConsistencyStrategy is used"); + + if (strategy == Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion) + { + Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should be set for LastCommittedWriteRegion on a read operation"); + } + else + { + Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + $"Hub region header should NOT be set for {strategyName} strategy"); + } + return TestHandler.ReturnSuccess(); }); @@ -862,20 +875,82 @@ public async Task ReadConsistencyStrategyRequestOptionSetsHeaders(string strateg } [TestMethod] - public async Task ReadConsistencyStrategyClientLevelApplied() + [DataRow("Query", DisplayName = "Query sets hub header")] + [DataRow("SqlQuery", DisplayName = "SqlQuery sets hub header")] + [DataRow("ReadFeed", DisplayName = "ReadFeed (ChangeFeed) sets hub header")] + public async Task LastCommittedWriteRegionSetsHubHeaderForNonPointReadOperations(string operationTypeName) + { + OperationType operationType = Enum.Parse(operationTypeName); + using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient(accountConsistencyLevel: Cosmos.ConsistencyLevel.Strong); + + TestHandler testHandler = new TestHandler((request, cancellationToken) => + { + Assert.AreEqual( + Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), + request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy], + "ReadConsistencyStrategy header should be set"); + Assert.AreEqual( + bool.TrueString, + request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + $"Hub region header should be set for LastCommittedWriteRegion on {operationTypeName}"); + + return TestHandler.ReturnSuccess(); + }); + + RequestInvokerHandler invoker = new RequestInvokerHandler( + client, + requestedClientConsistencyLevel: null, + requestedClientReadConsistencyStrategy: null, + requestedClientPriorityLevel: null, + requestedClientThroughputBucket: null) + { + InnerHandler = testHandler + }; + + RequestMessage requestMessage = new RequestMessage(HttpMethod.Get, new System.Uri("https://dummy.documents.azure.com:443/dbs")) + { + ResourceType = ResourceType.Document + }; + requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); + requestMessage.OperationType = operationType; + requestMessage.RequestOptions = new ItemRequestOptions + { + ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion + }; + + await invoker.SendAsync(requestMessage, new CancellationToken()); + } + + [TestMethod] + [DataRow("LatestCommitted")] + [DataRow("LastCommittedWriteRegion")] + public async Task ReadConsistencyStrategyClientLevelApplied(string strategyName) { // Verify client-level ReadConsistencyStrategy is applied when no request-level is set. + Cosmos.ReadConsistencyStrategy strategy = Enum.Parse(strategyName); using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient( accountConsistencyLevel: Cosmos.ConsistencyLevel.Strong, - customizeClientBuilder: builder => builder.WithReadConsistencyStrategy(Cosmos.ReadConsistencyStrategy.LatestCommitted)); + customizeClientBuilder: builder => builder.WithReadConsistencyStrategy(strategy)); TestHandler testHandler = new TestHandler((request, cancellationToken) => { Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.LatestCommitted.ToString(), + strategy.ToString(), request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy]); Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ConsistencyLevel], "ConsistencyLevel header should not be set when ReadConsistencyStrategy is used"); + + if (strategy == Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion) + { + Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should be set when client-level LastCommittedWriteRegion is used on a read"); + } + else + { + Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + $"Hub region header should NOT be set for {strategyName} strategy"); + } + return TestHandler.ReturnSuccess(); }); @@ -984,5 +1059,166 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() await invoker.SendAsync(requestMessage, new CancellationToken()); } + [TestMethod] + public async Task LastCommittedWriteRegionOnWriteOperation_NoHubHeader() + { + // LastCommittedWriteRegion on a write operation should NOT set the hub region header. + using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient(accountConsistencyLevel: Cosmos.ConsistencyLevel.Strong); + + TestHandler testHandler = new TestHandler((request, cancellationToken) => + { + Assert.AreEqual( + Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), + request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy], + "ReadConsistencyStrategy header should still be set on writes"); + Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should NOT be set for write operations even with LastCommittedWriteRegion"); + return TestHandler.ReturnSuccess(); + }); + + RequestInvokerHandler invoker = new RequestInvokerHandler( + client, + requestedClientConsistencyLevel: null, + requestedClientReadConsistencyStrategy: null, + requestedClientPriorityLevel: null, + requestedClientThroughputBucket: null) + { + InnerHandler = testHandler + }; + + RequestMessage requestMessage = new RequestMessage(HttpMethod.Post, new System.Uri("https://dummy.documents.azure.com:443/dbs")) + { + ResourceType = ResourceType.Document + }; + requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); + requestMessage.OperationType = OperationType.Create; + requestMessage.RequestOptions = new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }; + + await invoker.SendAsync(requestMessage, new CancellationToken()); + } + + [TestMethod] + public async Task NoReadConsistencyStrategy_HubRegionHeaderNotSet() + { + // Verify hub region header is NOT set when ReadConsistencyStrategy is not specified. + using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient(accountConsistencyLevel: Cosmos.ConsistencyLevel.Session); + + TestHandler testHandler = new TestHandler((request, cancellationToken) => + { + Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy], + "ReadConsistencyStrategy header should not be set"); + Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should not be set when ReadConsistencyStrategy is not used"); + return TestHandler.ReturnSuccess(); + }); + + RequestInvokerHandler invoker = new RequestInvokerHandler( + client, + requestedClientConsistencyLevel: Cosmos.ConsistencyLevel.Session, + requestedClientReadConsistencyStrategy: null, + requestedClientPriorityLevel: null, + requestedClientThroughputBucket: null) + { + InnerHandler = testHandler + }; + + RequestMessage requestMessage = new RequestMessage(HttpMethod.Get, new System.Uri("https://dummy.documents.azure.com:443/dbs")) + { + ResourceType = ResourceType.Document + }; + requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); + requestMessage.OperationType = OperationType.Read; + + await invoker.SendAsync(requestMessage, new CancellationToken()); + } + + [TestMethod] + public async Task LastCommittedWriteRegionWithConsistencyLevel_BothHeadersAndHubRegionSet() + { + // When both ConsistencyLevel and LastCommittedWriteRegion are set, + // all three headers should be present on a read operation. + using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient(accountConsistencyLevel: Cosmos.ConsistencyLevel.Strong); + + TestHandler testHandler = new TestHandler((request, cancellationToken) => + { + Assert.AreEqual( + Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), + request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy], + "ReadConsistencyStrategy header should be set"); + Assert.AreEqual( + Cosmos.ConsistencyLevel.Eventual.ToString(), + request.Headers[HttpConstants.HttpHeaders.ConsistencyLevel], + "ConsistencyLevel header should also be set"); + Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should be set for LastCommittedWriteRegion on a read"); + return TestHandler.ReturnSuccess(); + }); + + RequestInvokerHandler invoker = new RequestInvokerHandler( + client, + requestedClientConsistencyLevel: null, + requestedClientReadConsistencyStrategy: null, + requestedClientPriorityLevel: null, + requestedClientThroughputBucket: null) + { + InnerHandler = testHandler + }; + + RequestMessage requestMessage = new RequestMessage(HttpMethod.Get, new System.Uri("https://dummy.documents.azure.com:443/dbs")) + { + ResourceType = ResourceType.Document + }; + requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); + requestMessage.OperationType = OperationType.Read; + requestMessage.RequestOptions = new ItemRequestOptions + { + ConsistencyLevel = Cosmos.ConsistencyLevel.Eventual, + ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion + }; + + await invoker.SendAsync(requestMessage, new CancellationToken()); + } + + [TestMethod] + public async Task LastCommittedWriteRegionRequestLevel_OverridesClientLevel_HubHeaderSet() + { + // Request-level LastCommittedWriteRegion should override client-level Eventual and set hub header. + using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient( + accountConsistencyLevel: Cosmos.ConsistencyLevel.Strong, + customizeClientBuilder: builder => builder.WithReadConsistencyStrategy(Cosmos.ReadConsistencyStrategy.Eventual)); + + TestHandler testHandler = new TestHandler((request, cancellationToken) => + { + Assert.AreEqual( + Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), + request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy]); + Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ConsistencyLevel], + "ConsistencyLevel header should not be set when no ConsistencyLevel was specified"); + Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should be set when request-level LastCommittedWriteRegion overrides client-level"); + return TestHandler.ReturnSuccess(); + }); + + RequestInvokerHandler invoker = new RequestInvokerHandler( + client, + requestedClientConsistencyLevel: null, + requestedClientReadConsistencyStrategy: client.ClientOptions.ReadConsistencyStrategy, + requestedClientPriorityLevel: null, + requestedClientThroughputBucket: null) + { + InnerHandler = testHandler + }; + + RequestMessage requestMessage = new RequestMessage(HttpMethod.Get, new System.Uri("https://dummy.documents.azure.com:443/dbs")) + { + ResourceType = ResourceType.Document + }; + requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); + requestMessage.OperationType = OperationType.Read; + requestMessage.RequestOptions = new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }; + + await invoker.SendAsync(requestMessage, new CancellationToken()); + } + } -} \ No newline at end of file +}