From b5e72b2218e2f8d22e4280dc47959ed232f8c7d4 Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Tue, 28 Apr 2026 07:50:46 -0700 Subject: [PATCH 1/4] Add hub region header for read consistency strategy --- .../src/Handler/RequestInvokerHandler.cs | 4 + .../CosmosItemTests.cs | 272 ++++++++++++++++++ .../HandlerTests.cs | 45 ++- 3 files changed, 320 insertions(+), 1 deletion(-) diff --git a/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs b/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs index f3141bd675..41271be943 100644 --- a/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs +++ b/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs @@ -529,6 +529,10 @@ private Task ValidateAndSetReadConsistencyStrategyAsync(RequestMessage requestMe requestMessage.Headers.Set( HttpConstants.HttpHeaders.ReadConsistencyStrategy, readConsistencyStrategy.Value.ToString()); + + requestMessage.Headers.Set( + HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, + bool.TrueString); } return Task.CompletedTask; 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..b2c625675e 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -4422,6 +4422,278 @@ public async Task ReadItemAsync_ShouldAddHubHeader_OnRetryAfter_404_1002() Assert.AreEqual(2, return404Count, "Both requests should have returned 404/1002"); } + /// + /// Verifies that when ReadConsistencyStrategy is set on ItemRequestOptions, + /// both the hub region header and the ReadConsistencyStrategy header are present + /// from the very first request and persist through all 403/3 retries. + /// Parameterized over strategy values and 403/3 counts to cover: + /// - Each strategy triggers the correct headers (Session, Eventual, LatestCommitted) + /// - Single 403/3 retry (non-hub region rejects once, then reaches hub) + /// - Multiple 403/3 retries (cycles through several non-hub regions before hub) + /// + [TestMethod] + [Owner("aavasthy")] + [DataRow("Session", 1, DisplayName = "Request-level Session: single 403/3 → success")] + [DataRow("Eventual", 1, DisplayName = "Request-level Eventual: single 403/3 → success")] + [DataRow("LatestCommitted", 1, DisplayName = "Request-level LatestCommitted: single 403/3 → success")] + [DataRow("Session", 3, DisplayName = "Request-level Session: multiple 403/3 → success")] + [Description("Request-level ReadConsistencyStrategy with 403/3 retry flow.")] + public async Task ReadItemAsync_WithRequestLevelReadConsistencyStrategy_403_3_ThenSuccess(string strategyName, int forbiddenCount) + { + Cosmos.ReadConsistencyStrategy strategy = + (Cosmos.ReadConsistencyStrategy)Enum.Parse(typeof(Cosmos.ReadConsistencyStrategy), strategyName); + + 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(HubRegionHeader, 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); + + // Return 403/3 to simulate non-hub region rejection + 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)); + + // Act: Read with ReadConsistencyStrategy set at request level + ItemResponse response = await customContainer.ReadItemAsync( + testItem.id, + new Cosmos.PartitionKey(testItem.pk), + new ItemRequestOptions { ReadConsistencyStrategy = strategy }); + + // Assert: Request succeeded after 403/3 retries + 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}."); + + // Verify both headers were present on EVERY request + for (int i = 0; i < hubHeaderPerRequest.Count; i++) + { + Assert.IsTrue(hubHeaderPerRequest[i], + $"Hub region header MUST be present on request #{i + 1} when ReadConsistencyStrategy={strategyName} is set."); + Assert.AreEqual(strategyName, rcsHeaderPerRequest[i], + $"ReadConsistencyStrategy header MUST be '{strategyName}' on request #{i + 1}."); + } + } + + /// + /// Verifies that when ReadConsistencyStrategy is set at the CosmosClientOptions level, + /// the hub region header is present on the first request and the + /// 403/3 retry flow succeeds. Parameterized over strategies to ensure client-level + /// propagation works for all values. + /// + [TestMethod] + [Owner("aavasthy")] + [DataRow("Session", DisplayName = "Client-level Session strategy: 403/3 → success")] + [DataRow("Eventual", DisplayName = "Client-level Eventual strategy: 403/3 → success")] + [DataRow("LatestCommitted", DisplayName = "Client-level LatestCommitted strategy: 403/3 → success")] + [Description("Client-level ReadConsistencyStrategy causes hub header on first request with 403/3 then success.")] + public async Task ReadItemAsync_WithClientLevelReadConsistencyStrategy_403_3_ThenSuccess(string strategyName) + { + Cosmos.ReadConsistencyStrategy strategy = + (Cosmos.ReadConsistencyStrategy)Enum.Parse(typeof(Cosmos.ReadConsistencyStrategy), strategyName); + + int docReadRequestCount = 0; + int return403Count = 0; + const int maxReturn403 = 1; + bool hubHeaderOnFirstRequest = false; + string readConsistencyStrategyHeaderValue = null; + + 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(HubRegionHeader, out IEnumerable values) + && values.Any(); + + if (docReadRequestCount == 1) + { + hubHeaderOnFirstRequest = hasHubHeader; + + if (request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues)) + { + readConsistencyStrategyHeaderValue = rcsValues.FirstOrDefault(); + } + } + + 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 = strategy + }; + + 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)); + + // Act: 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.IsTrue(hubHeaderOnFirstRequest, + $"Hub region header MUST be present on the first request when client-level ReadConsistencyStrategy={strategyName} is set."); + Assert.AreEqual(strategyName, readConsistencyStrategyHeaderValue, + $"ReadConsistencyStrategy header should be '{strategyName}'."); + Assert.AreEqual(maxReturn403, return403Count, "Should have returned 403/3 exactly once."); + Assert.IsTrue(docReadRequestCount >= 2, + $"Expected at least 2 requests, got {docReadRequestCount}."); + } + + /// + /// Verifies that when ReadConsistencyStrategy is NOT set, the hub header is NOT present + /// on the initial request. This is the negative/control test confirming that the hub header + /// is only added when ReadConsistencyStrategy triggers it in RequestInvokerHandler. + /// + [TestMethod] + [Owner("aavasthy")] + [Description("Without ReadConsistencyStrategy, hub header is NOT present on initial request.")] + public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHubHeaderOnFirstRequest() + { + 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(HubRegionHeader, out IEnumerable values) + && values.Any(); + + rcsHeaderOnFirstRequest = request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues) + && rcsValues.Any(); + } + + // Let all requests pass through to the emulator + 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)); + + // Act: Read WITHOUT ReadConsistencyStrategy + 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/HandlerTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs index 219260a587..46a6a389b7 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs @@ -837,6 +837,8 @@ 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"); + Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should be set when ReadConsistencyStrategy is used"); return TestHandler.ReturnSuccess(); }); @@ -876,6 +878,8 @@ public async Task ReadConsistencyStrategyClientLevelApplied() request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy]); Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ConsistencyLevel], "ConsistencyLevel header should not be set when ReadConsistencyStrategy is used"); + Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], + "Hub region header should be set when client-level ReadConsistencyStrategy is used"); return TestHandler.ReturnSuccess(); }); @@ -916,6 +920,8 @@ public async Task ReadConsistencyStrategyAndConsistencyLevelBothSetAtRequestLeve 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 when ReadConsistencyStrategy is used"); return TestHandler.ReturnSuccess(); }); @@ -960,6 +966,8 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() 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 ReadConsistencyStrategy is used"); return TestHandler.ReturnSuccess(); }); @@ -984,5 +992,40 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() 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()); + } + } -} \ No newline at end of file +} From 57e2e025cd339312f861996ab33936be20fe2e98 Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Wed, 29 Apr 2026 14:14:43 -0700 Subject: [PATCH 2/4] Only setting hub region header for LastCommittedWriteRegion ReadConsistencyStrategy and only for read operation type --- .../src/Handler/RequestInvokerHandler.cs | 13 +- .../Settings/ReadConsistencyStrategy.cs | 9 +- .../CosmosItemTests.cs | 170 +++++++++++------- .../HandlerTests.cs | 73 ++++++-- 4 files changed, 189 insertions(+), 76 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs b/Microsoft.Azure.Cosmos/src/Handler/RequestInvokerHandler.cs index 41271be943..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) { @@ -530,9 +533,13 @@ private Task ValidateAndSetReadConsistencyStrategyAsync(RequestMessage requestMe HttpConstants.HttpHeaders.ReadConsistencyStrategy, readConsistencyStrategy.Value.ToString()); - requestMessage.Headers.Set( - HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, - bool.TrueString); + 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..a3c2da5ba6 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs @@ -48,6 +48,13 @@ 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, + + /// + /// When set, the SDK automatically adds the hub region processing header so that + /// the backend directs the request to the hub region. Non-hub regions respond with + /// 403/3 (WriteForbidden), which the SDK retries transparently. + /// + 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 b2c625675e..0c9b86abdf 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -4423,26 +4423,19 @@ public async Task ReadItemAsync_ShouldAddHubHeader_OnRetryAfter_404_1002() } /// - /// Verifies that when ReadConsistencyStrategy is set on ItemRequestOptions, - /// both the hub region header and the ReadConsistencyStrategy header are present - /// from the very first request and persist through all 403/3 retries. - /// Parameterized over strategy values and 403/3 counts to cover: - /// - Each strategy triggers the correct headers (Session, Eventual, LatestCommitted) - /// - Single 403/3 retry (non-hub region rejects once, then reaches hub) - /// - Multiple 403/3 retries (cycles through several non-hub regions before hub) + /// 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("Session", 1, DisplayName = "Request-level Session: single 403/3 → success")] - [DataRow("Eventual", 1, DisplayName = "Request-level Eventual: single 403/3 → success")] - [DataRow("LatestCommitted", 1, DisplayName = "Request-level LatestCommitted: single 403/3 → success")] - [DataRow("Session", 3, DisplayName = "Request-level Session: multiple 403/3 → success")] - [Description("Request-level ReadConsistencyStrategy with 403/3 retry flow.")] - public async Task ReadItemAsync_WithRequestLevelReadConsistencyStrategy_403_3_ThenSuccess(string strategyName, int forbiddenCount) + [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) { - Cosmos.ReadConsistencyStrategy strategy = - (Cosmos.ReadConsistencyStrategy)Enum.Parse(typeof(Cosmos.ReadConsistencyStrategy), strategyName); - int docReadRequestCount = 0; int return403Count = 0; List hubHeaderPerRequest = new List(); @@ -4469,7 +4462,6 @@ public async Task ReadItemAsync_WithRequestLevelReadConsistencyStrategy_403_3_Th } rcsHeaderPerRequest.Add(rcsValue); - // Return 403/3 to simulate non-hub region rejection if (return403Count < forbiddenCount) { return403Count++; @@ -4506,13 +4498,11 @@ public async Task ReadItemAsync_WithRequestLevelReadConsistencyStrategy_403_3_Th ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); - // Act: Read with ReadConsistencyStrategy set at request level ItemResponse response = await customContainer.ReadItemAsync( testItem.id, new Cosmos.PartitionKey(testItem.pk), - new ItemRequestOptions { ReadConsistencyStrategy = strategy }); + new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }); - // Assert: Request succeeded after 403/3 retries Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); Assert.IsNotNull(response.Resource); Assert.AreEqual(testItem.id, response.Resource.id); @@ -4521,38 +4511,30 @@ public async Task ReadItemAsync_WithRequestLevelReadConsistencyStrategy_403_3_Th Assert.IsTrue(docReadRequestCount >= forbiddenCount + 1, $"Expected at least {forbiddenCount + 1} requests ({forbiddenCount}x 403/3 + 1x success), got {docReadRequestCount}."); - // Verify both headers were present on EVERY request for (int i = 0; i < hubHeaderPerRequest.Count; i++) { Assert.IsTrue(hubHeaderPerRequest[i], - $"Hub region header MUST be present on request #{i + 1} when ReadConsistencyStrategy={strategyName} is set."); - Assert.AreEqual(strategyName, rcsHeaderPerRequest[i], - $"ReadConsistencyStrategy header MUST be '{strategyName}' on request #{i + 1}."); + $"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 is set at the CosmosClientOptions level, - /// the hub region header is present on the first request and the - /// 403/3 retry flow succeeds. Parameterized over strategies to ensure client-level - /// propagation works for all values. + /// 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")] - [DataRow("Session", DisplayName = "Client-level Session strategy: 403/3 → success")] - [DataRow("Eventual", DisplayName = "Client-level Eventual strategy: 403/3 → success")] - [DataRow("LatestCommitted", DisplayName = "Client-level LatestCommitted strategy: 403/3 → success")] - [Description("Client-level ReadConsistencyStrategy causes hub header on first request with 403/3 then success.")] - public async Task ReadItemAsync_WithClientLevelReadConsistencyStrategy_403_3_ThenSuccess(string strategyName) + [Description("Client-level LastCommittedWriteRegion sets hub header on reads with 403/3 then success.")] + public async Task ReadItemAsync_WithLastCommittedWriteRegion_ClientLevel_403_3_ThenSuccess() { - Cosmos.ReadConsistencyStrategy strategy = - (Cosmos.ReadConsistencyStrategy)Enum.Parse(typeof(Cosmos.ReadConsistencyStrategy), strategyName); - int docReadRequestCount = 0; int return403Count = 0; const int maxReturn403 = 1; - bool hubHeaderOnFirstRequest = false; - string readConsistencyStrategyHeaderValue = null; + List hubHeaderPerRequest = new List(); + List rcsHeaderPerRequest = new List(); HttpClientHandlerHelper httpHandler = new HttpClientHandlerHelper { @@ -4566,16 +4548,14 @@ public async Task ReadItemAsync_WithClientLevelReadConsistencyStrategy_403_3_The bool hasHubHeader = request.Headers.TryGetValues(HubRegionHeader, out IEnumerable values) && values.Any(); + hubHeaderPerRequest.Add(hasHubHeader); - if (docReadRequestCount == 1) + string rcsValue = null; + if (request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues)) { - hubHeaderOnFirstRequest = hasHubHeader; - - if (request.Headers.TryGetValues("x-ms-cosmos-read-consistency-strategy", out IEnumerable rcsValues)) - { - readConsistencyStrategyHeaderValue = rcsValues.FirstOrDefault(); - } + rcsValue = rcsValues.FirstOrDefault(); } + rcsHeaderPerRequest.Add(rcsValue); if (return403Count < maxReturn403) { @@ -4605,7 +4585,7 @@ public async Task ReadItemAsync_WithClientLevelReadConsistencyStrategy_403_3_The ConnectionMode = ConnectionMode.Gateway, ConsistencyLevel = Cosmos.ConsistencyLevel.Session, HttpClientFactory = () => new HttpClient(httpHandler), - ReadConsistencyStrategy = strategy + ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }; using CosmosClient customClient = TestCommon.CreateCosmosClient(clientOptions); @@ -4614,32 +4594,104 @@ public async Task ReadItemAsync_WithClientLevelReadConsistencyStrategy_403_3_The ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); - // Act: Read without per-request options — client-level strategy should take effect + // 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(HubRegionHeader, 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.IsTrue(hubHeaderOnFirstRequest, - $"Hub region header MUST be present on the first request when client-level ReadConsistencyStrategy={strategyName} is set."); - Assert.AreEqual(strategyName, readConsistencyStrategyHeaderValue, + 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}'."); - Assert.AreEqual(maxReturn403, return403Count, "Should have returned 403/3 exactly once."); - Assert.IsTrue(docReadRequestCount >= 2, - $"Expected at least 2 requests, got {docReadRequestCount}."); } /// - /// Verifies that when ReadConsistencyStrategy is NOT set, the hub header is NOT present - /// on the initial request. This is the negative/control test confirming that the hub header - /// is only added when ReadConsistencyStrategy triggers it in RequestInvokerHandler. + /// 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, hub header is NOT present on initial request.")] - public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHubHeaderOnFirstRequest() + [Description("Without ReadConsistencyStrategy, neither hub header nor RCS header is present.")] + public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHeaders() { bool hubHeaderOnFirstRequest = false; bool rcsHeaderOnFirstRequest = false; @@ -4663,7 +4715,6 @@ public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHubHeaderOnFirs && rcsValues.Any(); } - // Let all requests pass through to the emulator return Task.FromResult(null); } }; @@ -4681,7 +4732,6 @@ public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHubHeaderOnFirs ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); await this.Container.CreateItemAsync(testItem, new Cosmos.PartitionKey(testItem.pk)); - // Act: Read WITHOUT ReadConsistencyStrategy ItemResponse response = await customContainer.ReadItemAsync( testItem.id, new Cosmos.PartitionKey(testItem.pk)); 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 46a6a389b7..a07581de98 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,8 +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"); - Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], - "Hub region header should 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(); }); @@ -869,17 +880,17 @@ public async Task ReadConsistencyStrategyClientLevelApplied() // Verify client-level ReadConsistencyStrategy is applied when no request-level is set. using CosmosClient client = MockCosmosUtil.CreateMockCosmosClient( accountConsistencyLevel: Cosmos.ConsistencyLevel.Strong, - customizeClientBuilder: builder => builder.WithReadConsistencyStrategy(Cosmos.ReadConsistencyStrategy.LatestCommitted)); + customizeClientBuilder: builder => builder.WithReadConsistencyStrategy(Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion)); TestHandler testHandler = new TestHandler((request, cancellationToken) => { Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.LatestCommitted.ToString(), + Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy]); Assert.IsNull(request.Headers[HttpConstants.HttpHeaders.ConsistencyLevel], "ConsistencyLevel header should not be set when ReadConsistencyStrategy is used"); Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], - "Hub region header should be set when client-level ReadConsistencyStrategy is used"); + "Hub region header should be set when client-level LastCommittedWriteRegion is used on a read"); return TestHandler.ReturnSuccess(); }); @@ -913,7 +924,7 @@ public async Task ReadConsistencyStrategyAndConsistencyLevelBothSetAtRequestLeve TestHandler testHandler = new TestHandler((request, cancellationToken) => { Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.Session.ToString(), + Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), request.Headers[HttpConstants.HttpHeaders.ReadConsistencyStrategy], "ReadConsistencyStrategy header should be set"); Assert.AreEqual( @@ -921,7 +932,7 @@ public async Task ReadConsistencyStrategyAndConsistencyLevelBothSetAtRequestLeve 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 when ReadConsistencyStrategy is used"); + "Hub region header should be set for LastCommittedWriteRegion on a read"); return TestHandler.ReturnSuccess(); }); @@ -944,7 +955,7 @@ public async Task ReadConsistencyStrategyAndConsistencyLevelBothSetAtRequestLeve requestMessage.RequestOptions = new ItemRequestOptions { ConsistencyLevel = Cosmos.ConsistencyLevel.Eventual, - ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.Session + ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }; await invoker.SendAsync(requestMessage, new CancellationToken()); @@ -960,14 +971,14 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() TestHandler testHandler = new TestHandler((request, cancellationToken) => { - // Request-level Session should override client-level Eventual + // Request-level LastCommittedWriteRegion should override client-level Eventual Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.Session.ToString(), + 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 ReadConsistencyStrategy is used"); + "Hub region header should be set when request-level LastCommittedWriteRegion overrides client-level"); return TestHandler.ReturnSuccess(); }); @@ -987,7 +998,45 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() }; requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); requestMessage.OperationType = OperationType.Read; - requestMessage.RequestOptions = new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.Session }; + requestMessage.RequestOptions = new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }; + + 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()); } From b0090874b208e77e7da622c0d6a0ec011230fc7a Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Wed, 29 Apr 2026 14:27:44 -0700 Subject: [PATCH 3/4] Update tests --- .../HandlerTests.cs | 102 ++++++++++++++++-- 1 file changed, 93 insertions(+), 9 deletions(-) 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 a07581de98..d0ac29cc3f 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs @@ -924,15 +924,13 @@ public async Task ReadConsistencyStrategyAndConsistencyLevelBothSetAtRequestLeve TestHandler testHandler = new TestHandler((request, cancellationToken) => { Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), + Cosmos.ReadConsistencyStrategy.Session.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(); }); @@ -955,7 +953,7 @@ public async Task ReadConsistencyStrategyAndConsistencyLevelBothSetAtRequestLeve requestMessage.RequestOptions = new ItemRequestOptions { ConsistencyLevel = Cosmos.ConsistencyLevel.Eventual, - ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion + ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.Session }; await invoker.SendAsync(requestMessage, new CancellationToken()); @@ -971,14 +969,12 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() TestHandler testHandler = new TestHandler((request, cancellationToken) => { - // Request-level LastCommittedWriteRegion should override client-level Eventual + // Request-level Session should override client-level Eventual Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.ToString(), + Cosmos.ReadConsistencyStrategy.Session.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(); }); @@ -998,7 +994,7 @@ public async Task ReadConsistencyStrategyRequestLevelOverridesClientLevel() }; requestMessage.Headers.Add(HttpConstants.HttpHeaders.PartitionKey, "[]"); requestMessage.OperationType = OperationType.Read; - requestMessage.RequestOptions = new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion }; + requestMessage.RequestOptions = new ItemRequestOptions { ReadConsistencyStrategy = Cosmos.ReadConsistencyStrategy.Session }; await invoker.SendAsync(requestMessage, new CancellationToken()); } @@ -1076,5 +1072,93 @@ public async Task NoReadConsistencyStrategy_HubRegionHeaderNotSet() 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()); + } + } } From 5bcbbaa60311f2dca64b3e73c17013b7f9160f19 Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Thu, 30 Apr 2026 01:37:02 -0700 Subject: [PATCH 4/4] Update contracts and fix tests --- .../Settings/ReadConsistencyStrategy.cs | 5 +- .../CosmosItemTests.cs | 11 ++- .../ClientRetryPolicyTests.cs | 9 ++- .../Contracts/DotNetPreviewSDKAPI.net6.json | 5 ++ .../HandlerTests.cs | 70 +++++++++++++++++-- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs index a3c2da5ba6..c81a4a3342 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/ReadConsistencyStrategy.cs @@ -51,9 +51,8 @@ enum ReadConsistencyStrategy GlobalStrong = 4, /// - /// When set, the SDK automatically adds the hub region processing header so that - /// the backend directs the request to the hub region. Non-hub regions respond with - /// 403/3 (WriteForbidden), which the SDK retries transparently. + /// 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 0c9b86abdf..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."); @@ -4451,7 +4450,7 @@ public async Task ReadItemAsync_WithLastCommittedWriteRegion_RequestLevel_403_3_ { docReadRequestCount++; - bool hasHubHeader = request.Headers.TryGetValues(HubRegionHeader, out IEnumerable values) + bool hasHubHeader = request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable values) && values.Any(); hubHeaderPerRequest.Add(hasHubHeader); @@ -4546,7 +4545,7 @@ public async Task ReadItemAsync_WithLastCommittedWriteRegion_ClientLevel_403_3_T { docReadRequestCount++; - bool hasHubHeader = request.Headers.TryGetValues(HubRegionHeader, out IEnumerable values) + bool hasHubHeader = request.Headers.TryGetValues(HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion, out IEnumerable values) && values.Any(); hubHeaderPerRequest.Add(hasHubHeader); @@ -4645,7 +4644,7 @@ public async Task ReadItemAsync_WithNonLastCommittedWriteRegionStrategy_NoHubHea { interceptedFirstRequest = true; - hubHeaderOnFirstRequest = request.Headers.TryGetValues(HubRegionHeader, out IEnumerable values) + 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)) @@ -4708,7 +4707,7 @@ public async Task ReadItemAsync_WithoutReadConsistencyStrategy_NoHeaders() { interceptedFirstRequest = true; - hubHeaderOnFirstRequest = request.Headers.TryGetValues(HubRegionHeader, out IEnumerable values) + 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) 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 d0ac29cc3f..5b09b9e189 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/HandlerTests.cs @@ -875,22 +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.LastCommittedWriteRegion)); + customizeClientBuilder: builder => builder.WithReadConsistencyStrategy(strategy)); TestHandler testHandler = new TestHandler((request, cancellationToken) => { Assert.AreEqual( - Cosmos.ReadConsistencyStrategy.LastCommittedWriteRegion.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"); - Assert.AreEqual(bool.TrueString, request.Headers[HttpConstants.HttpHeaders.ShouldProcessOnlyInHubRegion], - "Hub region header should be set when client-level LastCommittedWriteRegion is used on a read"); + + 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(); });