diff --git a/Microsoft.Azure.Cosmos/src/Handler/RequestMessage.cs b/Microsoft.Azure.Cosmos/src/Handler/RequestMessage.cs
index 5ecae3cb64..ceb5a07625 100644
--- a/Microsoft.Azure.Cosmos/src/Handler/RequestMessage.cs
+++ b/Microsoft.Azure.Cosmos/src/Handler/RequestMessage.cs
@@ -296,6 +296,9 @@ internal DocumentServiceRequest ToDocumentServiceRequest()
serviceRequest.UseStatusCodeForFailures = true;
serviceRequest.UseStatusCodeFor429 = true;
+ serviceRequest.UseStatusCodeFor4041002 = true;
+ serviceRequest.UseStatusCodeFor403 = true;
+ serviceRequest.UseStatusCodeForBadRequest = true;
serviceRequest.Properties = this.Properties;
this.DocumentServiceRequest = serviceRequest;
}
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 379de2faba..e8dd379563 100644
--- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs
@@ -3400,8 +3400,11 @@ public async Task VerifySessionNotFoundStatistics()
Assert.Fail("Should had thrown ReadSessionNotAvailable");
}
catch (CosmosException cosmosException)
- {
- Assert.IsTrue(cosmosException.Message.Contains("The read session is not available for the input session token."), cosmosException.Message);
+ {
+ Assert.AreEqual(StatusCodes.NotFound, cosmosException.StatusCode);
+ Assert.AreEqual(SubStatusCodes.ReadSessionNotAvailable, cosmosException.SubStatusCode);
+
+ Assert.IsTrue(cosmosException.Message.Contains("The read/write session is not available"), cosmosException.Message);
string exception = cosmosException.ToString();
Assert.IsTrue(exception.Contains("Point Operation Statistics"), exception);
}
diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ExceptionLessTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ExceptionLessTests.cs
new file mode 100644
index 0000000000..f46445fe1c
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ExceptionLessTests.cs
@@ -0,0 +1,181 @@
+namespace Microsoft.Azure.Cosmos
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Runtime.ExceptionServices;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Azure.Cosmos.Core.Trace;
+ using Microsoft.Azure.Cosmos.Diagnostics;
+ using Microsoft.Azure.Cosmos.SDK.EmulatorTests;
+ using Microsoft.Azure.Cosmos.Tracing.TraceData;
+ using Microsoft.Azure.Documents;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ ///
+ /// Tests for exception-less behavior in Cosmos DB operations.
+ ///
+ ///
+ /// Reference: https://msdata.visualstudio.com/CosmosDB/_git/CosmosDB?path=/Product/SDK/.net/Microsoft.Azure.Cosmos.Friends/tests/ExceptionLessTests.cs
+ ///
+ [VisualStudio.TestTools.UnitTesting.TestClass]
+ public class ExceptionLessTests
+ {
+ private readonly ConcurrentBag Exceptions = new();
+
+#nullable enable
+ private void ExceptionCaptureHandler(object? sender, FirstChanceExceptionEventArgs eventArgs)
+#nullable disable
+ {
+ this.Exceptions.Add(eventArgs.Exception);
+ }
+
+ [TestInitialize]
+ public void TestInit()
+ {
+ // Subscribe to the FirstChanceException event
+ AppDomain.CurrentDomain.FirstChanceException += this.ExceptionCaptureHandler;
+
+ TraceSource traceSource = (TraceSource)typeof(DefaultTrace).GetProperty("TraceSource").GetValue(null);
+ traceSource.Switch.Level = SourceLevels.All;
+ traceSource.Listeners.Clear();
+ traceSource.Listeners.Add(new ConsoleTraceListener());
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ // Subscribe to the FirstChanceException event
+ AppDomain.CurrentDomain.FirstChanceException -= this.ExceptionCaptureHandler;
+ this.Exceptions.Clear();
+ }
+
+ ///
+ /// Test for exception less behavior with session not found scenarios
+ ///
+ [TestMethod]
+ [Owner("kirankk")]
+ [DataRow(ConnectionMode.Gateway, Cosmos.ConsistencyLevel.Session)]
+ [DataRow(ConnectionMode.Direct, Cosmos.ConsistencyLevel.Session)]
+ public async Task SessionNotFoundTestAsync(ConnectionMode mode,
+ Cosmos.ConsistencyLevel consistencyLevel)
+ {
+ string databaseId = Guid.NewGuid().ToString();
+ string containerId = Guid.NewGuid().ToString();
+
+ CosmosClientOptions clientOptions = new CosmosClientOptions()
+ {
+ ConnectionMode = mode,
+ RequestTimeout = TimeSpan.FromHours(10),
+ EnableUpgradeConsistencyToLocalQuorum = true,
+ };
+
+ using (CosmosClient cosmosClient = TestCommon.CreateCosmosClient(clientOptions))
+ {
+ await cosmosClient.CreateDatabaseIfNotExistsAsync(databaseId);
+ await cosmosClient.GetDatabase(databaseId).CreateContainerIfNotExistsAsync(containerId, "/id");
+
+ await cosmosClient.InitializeContainersAsync(new List<(string, string)>() { (databaseId, containerId) }, CancellationToken.None);
+
+ Container container = cosmosClient.GetContainer(databaseId, containerId);
+ ContainerProperties containerProperties = await container.ReadContainerAsync();
+
+ TestObj testObj = new TestObj() { id = Guid.NewGuid().ToString() };
+
+ ItemResponse createRespMsg = await container.CreateItemAsync(testObj, new Cosmos.PartitionKey(testObj.id));
+
+ DocumentServiceRequest.DefaultUseStatusCodeFor4041002 = false;
+
+ Trace.TraceInformation($"{Environment.NewLine}First Read (may be cold start) (UseStatusCodeFor4041002={DocumentServiceRequest.DefaultUseStatusCodeFor4041002})");
+ ResponseMessage respMsg = await container.ReadItemStreamAsync(testObj.id, new Cosmos.PartitionKey(testObj.id),
+ new ItemRequestOptions() { ConsistencyLevel = consistencyLevel });
+ this.TraceResponseMessageAndAssert(respMsg);
+
+ string futureLsn = this.GetFutureLsn(respMsg.Headers.Session);
+ Trace.TraceInformation($"{Environment.NewLine}Second ReadFor404-1002 (UseStatusCodeFor4041002={DocumentServiceRequest.DefaultUseStatusCodeFor4041002}): {futureLsn}");
+ respMsg = await container.ReadItemStreamAsync(testObj.id, new Cosmos.PartitionKey(testObj.id),
+ new ItemRequestOptions() { SessionToken = futureLsn, ConsistencyLevel = consistencyLevel });
+ SummaryDiagnostics summaryDiagnostics1 = new SummaryDiagnostics(((CosmosTraceDiagnostics)respMsg.Diagnostics).Value);
+ this.TraceResponseMessageAndAssert(respMsg);
+
+ Trace.TraceInformation($"{Environment.NewLine}Third Read (rebase if-any) (UseStatusCodeFor4041002={DocumentServiceRequest.DefaultUseStatusCodeFor4041002})");
+ respMsg = await container.ReadItemStreamAsync(testObj.id, new Cosmos.PartitionKey(testObj.id),
+ new ItemRequestOptions() { ConsistencyLevel = consistencyLevel });
+ this.TraceResponseMessageAndAssert(respMsg);
+
+ DocumentServiceRequest.DefaultUseStatusCodeFor4041002 = true;
+
+ Trace.TraceInformation($"{Environment.NewLine}ReadFor404-1002 (UseStatusCodeFor4041002={DocumentServiceRequest.DefaultUseStatusCodeFor4041002}): {futureLsn}");
+ respMsg = await container.ReadItemStreamAsync(testObj.id, new Cosmos.PartitionKey(testObj.id),
+ new ItemRequestOptions() { SessionToken = futureLsn, ConsistencyLevel = consistencyLevel });
+ SummaryDiagnostics summaryDiagnostics2 = new SummaryDiagnostics(((CosmosTraceDiagnostics)respMsg.Diagnostics).Value);
+ this.TraceResponseMessageAndAssert(respMsg, expectedExceptionCount: 0);
+
+ Assert.IsTrue(summaryDiagnostics1.AllRegionsContacted.Value.SetEquals(summaryDiagnostics2.AllRegionsContacted.Value), $"AllRegionsContacted");
+ CollectionAssert.AreEquivalent(summaryDiagnostics1.GatewayRequestsSummary.Value, summaryDiagnostics2.GatewayRequestsSummary.Value, "GatewayRequestsSummary");
+
+ // Direct #retries are expected to be different (exception vs exceptionless flows)
+ if (mode == ConnectionMode.Direct)
+ {
+ CollectionAssert.AreEquivalent(summaryDiagnostics1.DirectRequestsSummary.Value.Keys, summaryDiagnostics2.DirectRequestsSummary.Value.Keys);
+ Assert.AreEqual(1, summaryDiagnostics1.DirectRequestsSummary.Value.Keys.Count);
+
+ (int statusCode, int subStatusCode) = summaryDiagnostics1.DirectRequestsSummary.Value.Keys.First();
+ int exceptionFlowRetryCount = summaryDiagnostics1.DirectRequestsSummary.Value[(statusCode, subStatusCode)];
+ int exceptionLessFlowRetryCount = summaryDiagnostics2.DirectRequestsSummary.Value[(statusCode, subStatusCode)];
+ Assert.IsTrue(exceptionFlowRetryCount == exceptionLessFlowRetryCount
+ || (exceptionLessFlowRetryCount > exceptionFlowRetryCount && ((exceptionLessFlowRetryCount - exceptionFlowRetryCount) / exceptionFlowRetryCount * 100) < 10),
+ $"DirectRequestsSummary: {string.Join(Environment.NewLine, summaryDiagnostics1.DirectRequestsSummary.Value.Select(e => $"{e.Key} -> {e.Value}"))} {Environment.NewLine} {string.Join(Environment.NewLine, summaryDiagnostics2.DirectRequestsSummary.Value.Select(e => $"{e.Key} -> {e.Value}"))}");
+ }
+
+ // Delete the database
+ await cosmosClient.GetDatabase(databaseId).DeleteAsync();
+ }
+ }
+
+ private string GetFutureLsn(string sessionTokenStr)
+ {
+ if (SessionTokenHelper.TryParse(sessionTokenStr, out string partitionKeyRangeId, out ISessionToken parsedSessionToken))
+ {
+ VectorSessionToken vectorSessionToken = (VectorSessionToken)parsedSessionToken;
+ if (vectorSessionToken != null)
+ {
+ ISessionToken futureSessionToken = new VectorSessionToken(vectorSessionToken, vectorSessionToken.LSN + 50);
+ return $"{partitionKeyRangeId}:{futureSessionToken.ConvertToString()}";
+ }
+ }
+
+ throw new ArgumentException($"Failed for {sessionTokenStr}");
+ }
+
+ private void TraceResponseMessageAndAssert(ResponseMessage respMsg,
+ int? expectedExceptionCount = null)
+ {
+ IEnumerable nonHttpExceptions = this.Exceptions.Select(e => e.StackTrace).Where(e => !e.Contains("System.Net.Http.HttpConnection"));
+ int currentExceptionCount = nonHttpExceptions.Count();
+ Trace.TraceInformation($"(StatusCode, SubStatusCode): {respMsg.StatusCode} -> {respMsg.Headers.SubStatusCode}");
+ Trace.TraceInformation($"SessionToken(Request -> Response): {respMsg.RequestMessage.Headers.Session} -> {respMsg.Headers.Session}");
+ Trace.TraceInformation($"Exception count: {currentExceptionCount}");
+ Trace.TraceInformation($"Distinct Msg's: {string.Join(Environment.NewLine, this.Exceptions.Select(e => e.Message).GroupBy(e => e, (gpkey, gpValues) => $"{gpkey} -> {gpValues.Count()}"))}");
+ Trace.TraceInformation(respMsg.Diagnostics.ToString());
+
+ if (expectedExceptionCount.HasValue)
+ {
+ Assert.AreEqual(expectedExceptionCount, currentExceptionCount,
+ $"{string.Join(Environment.NewLine, nonHttpExceptions.Distinct())}");
+ }
+
+ this.Exceptions.Clear();
+ }
+
+ public class TestObj
+ {
+#pragma warning disable SA1300 // Element should begin with upper-case letter
+ public string id { get; set; }
+#pragma warning restore SA1300 // Element should begin with upper-case letter
+ }
+ }
+}
diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs
index 4f87e51e19..3eed448dab 100644
--- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/SessionRetryOptionsTest.cs
@@ -97,7 +97,7 @@ public async Task ReadOrQueryOperationWithMaxInRegionRetryCountZero(FaultInjecti
// Assert that only the original attempt happened (no retries)
long hitCount = badSessionTokenRule.GetHitCount();
- Assert.AreEqual(4, hitCount, $"There should be only one attempt (no retries) for {faultInjectionOperationType} when MaxInRegionRetryCount is 0 and RemotePreferredRegion is set to true.");
+ Assert.AreEqual(4, hitCount, $"There should be only one attempt (no retries) for {faultInjectionOperationType} when MaxInRegionRetryCount is 0 and RemotePreferredRegion is set to true. Diagnostics {executionResult.Diagnostics}");
}
}
@@ -174,7 +174,7 @@ public async Task ReadOperationWithReadSessionUnavailableTest(FaultInjectionOper
if (remoteRegionPreferred)
{
- Assert.IsTrue(hitCount >= sessionTokenMismatchRetryAttempts && hitCount <= (1 + sessionTokenMismatchRetryAttempts) * 4);
+ Assert.IsTrue(hitCount >= sessionTokenMismatchRetryAttempts && hitCount <= (1 + sessionTokenMismatchRetryAttempts) * 4, executionResult.Diagnostics.ToString());
}
}
}
@@ -261,7 +261,7 @@ public async Task WriteOperationWithReadSessionUnavailableTest(FaultInjectionOpe
if (remoteRegionPreferred)
{
// higher hit count is possible while in MinRetryWaitTimeWithinRegion
- Assert.IsTrue(hitCount >= sessionTokenMismatchRetryAttempts);
+ Assert.IsTrue(hitCount >= sessionTokenMismatchRetryAttempts, executionResult.Diagnostics.ToString());
}
}
}