From e337f59413d0e0dc2d2c68969bb2d92e12622d5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 04:22:24 +0000 Subject: [PATCH 1/3] Document Cosmos Linux emulator failures behind the skip condition (issue #291) Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/4f55855e-7fc7-42f6-b8d2-537427c6beb8 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../01-invalid-session-token-error-message.md | 99 ++++++++++++++ .../02-future-session-token-ignored.md | 109 +++++++++++++++ .../03-failed-write-advances-session-token.md | 125 ++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md create mode 100644 docs/cosmos-emulator-issues/02-future-session-token-ignored.md create mode 100644 docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md diff --git a/docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md b/docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md new file mode 100644 index 00000000000..9cdd05d387a --- /dev/null +++ b/docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md @@ -0,0 +1,99 @@ +# Linux Cosmos DB emulator returns a different error message for an invalid session token + +Tracking issue: [Azure/azure-cosmos-db-emulator-docker#291](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291) + +## Summary + +When the SDK passes a syntactically-invalid value as the `SessionToken` request header, the **Linux** Cosmos DB emulator +(`mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview`) returns the error string + +> `The session token provided 'invalidtoken' is not valid.` + +The **Windows** emulator and the real Cosmos DB service instead return + +> `The session token provided 'invalidtoken' is invalid.` + +Both responses are `400 BadRequest`, but the wording is different (`is not valid` vs. `is invalid`). Code that +asserts on the exact message text (which is what we do in our EF Core tests) fails on the Linux emulator. + +This is the failure observed in 21 of the 26 EF Core +`CosmosSessionTokensTest` failures (the `Query_uses_session_token`, +`Read_item_uses_session_token`, `Shaped_query_uses_session_token`, +`PagingQuery_uses_session_token`, and all `Add_uses_GetSessionToken` / +`Update_uses_session_token` / `Delete_uses_session_token` theory rows). + +## Stand-alone repro (no EF Core) + +`Program.csproj`: + +```xml + + + Exe + net8.0 + + + + + +``` + +`Program.cs`: + +```csharp +using System.Net.Http; +using Microsoft.Azure.Cosmos; + +const string Endpoint = "https://localhost:8081"; +const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + +var options = new CosmosClientOptions +{ + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }) +}; + +using var client = new CosmosClient(Endpoint, Key, options); +var db = (await client.CreateDatabaseIfNotExistsAsync("ReproDb")).Database; +var container = (await db.CreateContainerIfNotExistsAsync( + new ContainerProperties("Repro", "/pk"))).Container; + +await container.UpsertItemAsync(new { id = "1", pk = "1" }, new PartitionKey("1")); + +try +{ + await container.ReadItemAsync( + "1", new PartitionKey("1"), + new ItemRequestOptions { SessionToken = "invalidtoken" }); +} +catch (CosmosException ex) +{ + // Real Cosmos / Windows emulator: + // "The session token provided 'invalidtoken' is invalid." + // Linux emulator: + // "The session token provided 'invalidtoken' is not valid." + Console.WriteLine(ex.ResponseBody); +} +``` + +Expected output (real Cosmos / Windows emulator): + +``` +code : BadRequest +message : The session token provided 'invalidtoken' is invalid. +``` + +Actual output on the Linux emulator: + +``` +code : BadRequest +message : The session token provided 'invalidtoken' is not valid. +``` + +## Suggested fix + +Align the Linux emulator's error message with the real service so that the substring `is invalid` is preserved. diff --git a/docs/cosmos-emulator-issues/02-future-session-token-ignored.md b/docs/cosmos-emulator-issues/02-future-session-token-ignored.md new file mode 100644 index 00000000000..3365ab7f9e4 --- /dev/null +++ b/docs/cosmos-emulator-issues/02-future-session-token-ignored.md @@ -0,0 +1,109 @@ +# Linux Cosmos DB emulator silently accepts an unreachable (future) session token + +Tracking issue: [Azure/azure-cosmos-db-emulator-docker#291](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291) + +## Summary + +When a client passes a session token whose LSN is far in the future (one that the server can never satisfy because no +such write has occurred), the **real Cosmos DB service** and the **Windows** emulator block briefly waiting for the +session to become available and then return `404 NotFound` with sub-status `1002` and the message + +> `The read session is not available for the input session token.` + +The **Linux** emulator (`mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview`) instead **silently +returns the item with status `200 OK`**, completely ignoring the session token. + +This causes the following EF Core `CosmosSessionTokensTest+CosmosNonSharedSessionTokenTests` tests to fail because the +expected `CosmosException` is never thrown: + +- `UseSessionTokens_uses_session_tokens` +- `Read_item_session_not_found_throws_CosmosException` + +## Stand-alone repro (no EF Core) + +`Program.csproj`: + +```xml + + + Exe + net8.0 + + + + + +``` + +`Program.cs`: + +```csharp +using System.Net.Http; +using Microsoft.Azure.Cosmos; + +const string Endpoint = "https://localhost:8081"; +const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + +var options = new CosmosClientOptions +{ + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }) +}; + +using var client = new CosmosClient(Endpoint, Key, options); +var db = (await client.CreateDatabaseIfNotExistsAsync("ReproDb")).Database; +var container = (await db.CreateContainerIfNotExistsAsync( + new ContainerProperties("ReproFuture", "/pk"))).Container; + +// Write an item to obtain a valid session token in the form ":<...>#". +var write = await container.UpsertItemAsync( + new { id = "1", pk = "1" }, new PartitionKey("1")); +var valid = write.Headers.Session; // e.g. "0:0#5" + +// Build a token in the same range but with LSN = int.MaxValue. +var hash = valid.IndexOf('#'); +var future = valid.Substring(0, hash + 1) + int.MaxValue; +Console.WriteLine($"valid = {valid}"); +Console.WriteLine($"future = {future}"); + +try +{ + var result = await container.ReadItemAsync( + "1", new PartitionKey("1"), + new ItemRequestOptions { SessionToken = future }); + + // Linux emulator: prints "200 OK" (ignored the unsatisfiable session token). + Console.WriteLine($"Read succeeded with StatusCode={result.StatusCode}"); +} +catch (CosmosException ex) +{ + // Real Cosmos / Windows emulator: 404 with the standard "read session is not available" message. + Console.WriteLine($"Status={ex.StatusCode} SubStatus={ex.SubStatusCode}"); + Console.WriteLine(ex.ResponseBody); +} +``` + +Expected output (real Cosmos / Windows emulator): + +``` +Status=NotFound SubStatus=1002 +... The read session is not available for the input session token. ... +``` + +Actual output on the Linux emulator: + +``` +Read succeeded with StatusCode=OK +``` + +## Suggested fix + +The emulator should honor the session-token contract: when a client sends a session token whose LSN is greater than the +current max global LSN for the target partition, the request must either (a) wait for the session to become available +up to the configured timeout, or (b) return `404 NotFound` with sub-status `1002` +(`The read session is not available for the input session token.`), as the real service does. Returning `200 OK` while +ignoring the session token breaks session-consistency guarantees for clients that rely on causal reads. diff --git a/docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md b/docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md new file mode 100644 index 00000000000..b72006333f9 --- /dev/null +++ b/docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md @@ -0,0 +1,125 @@ +# Linux Cosmos DB emulator returns an extra session-token LSN across multi-context concurrency sequences + +Tracking issue: [Azure/azure-cosmos-db-emulator-docker#291](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291) + +## Summary + +While running EF Core's `CosmosSessionTokensTest+CosmosNonSharedSessionTokenTests.Optimistic_concurrency_precondition_failure_updates_session_token` +against the **Linux** emulator we observe that the set of session tokens EF Core sees across a multi-client +optimistic-concurrency sequence contains **one extra LSN value** that the same sequence does not produce against the +**Windows** emulator or the real Cosmos DB service. + +Specifically, EF Core's `CompositeSessionToken` (which accumulates every distinct session-token response value the SDK +hands it during a logical sequence) collects: + +| Environment | Tokens observed (joined) | +| --- | --- | +| Real Cosmos / Windows emulator | `0:0#51,0:0#52,0:0#0,0:0#54` | +| Linux emulator | `0:0#51,0:0#52,0:0#0,0:0#53,0:0#54` | + +Note the extra `0:0#53` slipping in between `0:0#0` and `0:0#54`. That extra LSN was returned in a response (most likely +to a *failed* write — either the 412 from the stale-ETag `Replace`, or the 412/404 from the subsequent stale-ETag +`Delete`) that should not have advanced the partition's session LSN. + +The failing assertions are: + +- `Optimistic_concurrency_precondition_failure_updates_session_token(autoTransactionBehavior: Always)` +- `Optimistic_concurrency_precondition_failure_updates_session_token(autoTransactionBehavior: Never)` + +## Stand-alone repro (no EF Core) + +The simplest deterministic repro is a two-"client" sequence: +client A creates a document, client B reads then replaces it, then client A +tries to replace using its now-stale ETag (and again to delete with the stale ETag). Capture the session token +returned in each response and look for an LSN that only appears under the Linux emulator. + +`Program.csproj`: + +```xml + + + Exe + net8.0 + + + + + +``` + +`Program.cs`: + +```csharp +using System.Net.Http; +using Microsoft.Azure.Cosmos; + +const string Endpoint = "https://localhost:8081"; +const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + +var options = new CosmosClientOptions +{ + ConnectionMode = ConnectionMode.Gateway, + ConsistencyLevel = ConsistencyLevel.Session, + HttpClientFactory = () => new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }) +}; + +using var client = new CosmosClient(Endpoint, Key, options); +var db = (await client.CreateDatabaseIfNotExistsAsync("ReproDb")).Database; +var c = (await db.CreateContainerIfNotExistsAsync(new ContainerProperties("ReproEtag", "/pk"))).Container; + +void Log(string label, string? session) => Console.WriteLine($"{label,-12} session={session}"); + +// Client A creates +var a1 = await c.CreateItemAsync(new { id = "1", pk = "1", v = 1 }, new PartitionKey("1")); +var staleEtag = a1.ETag; +Log("A create", a1.Headers.Session); + +// Client B reads and updates +var b1 = await c.ReadItemAsync("1", new PartitionKey("1")); +Log("B read", b1.Headers.Session); + +var b2 = await c.ReplaceItemAsync(new { id = "1", pk = "1", v = 2 }, "1", new PartitionKey("1"), + new ItemRequestOptions { IfMatchEtag = b1.ETag }); +Log("B replace", b2.Headers.Session); +var latestSuccessfulSession = b2.Headers.Session; + +// Client A tries to replace using its stale ETag -> 412 (FAILED write) +try +{ + await c.ReplaceItemAsync(new { id = "1", pk = "1", v = 3 }, "1", new PartitionKey("1"), + new ItemRequestOptions { IfMatchEtag = staleEtag }); +} +catch (CosmosException ex) +{ + Log("A replace*", ex.Headers?.Session); + // EXPECTED (real Cosmos / Windows emulator): same as 'B replace' (failed write does not advance LSN). + // OBSERVED (Linux emulator): may differ by 1 LSN. +} + +// Client A tries to delete using its stale ETag -> 412 (FAILED write) +try +{ + await c.DeleteItemAsync("1", new PartitionKey("1"), + new ItemRequestOptions { IfMatchEtag = staleEtag }); +} +catch (CosmosException ex) +{ + Log("A delete*", ex.Headers?.Session); +} +``` + +Expected on the real service: every session token observed after `B replace` equals `latestSuccessfulSession` until the +next successful write. Observed on the Linux emulator: at least one of the failed-write responses returns a session +token with a higher LSN than `latestSuccessfulSession`, which is then accumulated by EF Core's `CompositeSessionToken` +and causes the equality assertion to fail. + +## Suggested fix + +A failed write (412 PreconditionFailed, 404 NotFound on Delete/Replace, etc.) must not bump the partition's session +LSN, and the response header `x-ms-session-token` must reflect the last committed LSN — matching the behaviour of the +real Cosmos DB service and the Windows emulator. Otherwise, callers relying on session-token equality across +contexts to verify causality and detect concurrency outcomes (as the EF Core Cosmos provider does) see spurious +extra session tokens. From 2546a196a1d6d8436f4e0a92b27b9a0a9133f76e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 04:31:08 +0000 Subject: [PATCH 2/3] Cosmos: relax invalid-session-token assertion to match Linux and Windows emulator wording Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/45f81279-a6cc-4d8e-8d4c-4c8b5ef2dc56 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../CosmosSessionTokensTest.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs index 2a99113b791..cbd20b6bea1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs @@ -7,8 +7,6 @@ namespace Microsoft.EntityFrameworkCore; -// https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291 (Session tokens not properly tracked) -[CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public class CosmosSessionTokensTest(CosmosSessionTokensTest.CosmosFixture fixture) : IClassFixture { private const string DatabaseName = nameof(CosmosSessionTokensTest); @@ -81,7 +79,7 @@ await Assert.ThrowsAsync(() => context.OtherContainerCustomers. foreach (var ex in exes) { - Assert.Contains("The session token provided 'invalidtoken' is invalid", ex.ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ex.ResponseBody); } } @@ -100,7 +98,7 @@ await Assert.ThrowsAsync(() => context.OtherContainerCustomers. foreach (var ex in exes) { - Assert.Contains("The session token provided 'invalidtoken' is invalid", ex.ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ex.ResponseBody); } } @@ -119,7 +117,7 @@ public virtual async Task Shaped_query_uses_session_token() foreach (var ex in exes) { - Assert.Contains("The session token provided 'invalidtoken' is invalid", ex.ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ex.ResponseBody); } } @@ -138,7 +136,7 @@ await Assert.ThrowsAsync(() => context.OtherContainerCustomers. foreach (var ex in exes) { - Assert.Contains("The session token provided 'invalidtoken' is invalid", ex.ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ex.ResponseBody); } } @@ -479,7 +477,7 @@ public virtual async Task Add_uses_GetSessionToken(AutoTransactionBehavior autoT var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Contains("The session token provided 'invalidtoken' is invalid.", ((CosmosException)ex.InnerException!).ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ((CosmosException)ex.InnerException!).ResponseBody); } [ConditionalTheory] @@ -510,7 +508,7 @@ public virtual async Task Update_uses_session_token(AutoTransactionBehavior auto var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Contains("The session token provided 'invalidtoken' is invalid.", ((CosmosException)ex.InnerException!).ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ((CosmosException)ex.InnerException!).ResponseBody); } [ConditionalTheory] @@ -541,7 +539,7 @@ public virtual async Task Delete_uses_session_token(AutoTransactionBehavior auto var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Contains("The session token provided 'invalidtoken' is invalid.", ((CosmosException)ex.InnerException!).ResponseBody); + Assert.Contains("The session token provided 'invalidtoken' is", ((CosmosException)ex.InnerException!).ResponseBody); } [ConditionalFact] From 923e6c610c19c792ccbb57e38c78aa606173cff9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 04:46:21 +0000 Subject: [PATCH 3/3] Cosmos: replace class-level Linux-emulator skip with per-test skips citing emulator issues #322 and #319; remove diagnostic md files Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/773a262b-df07-464f-9364-92c079d190c7 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../01-invalid-session-token-error-message.md | 99 -------------- .../02-future-session-token-ignored.md | 109 --------------- .../03-failed-write-advances-session-token.md | 125 ------------------ .../CosmosSessionTokensTest.cs | 8 +- 4 files changed, 6 insertions(+), 335 deletions(-) delete mode 100644 docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md delete mode 100644 docs/cosmos-emulator-issues/02-future-session-token-ignored.md delete mode 100644 docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md diff --git a/docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md b/docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md deleted file mode 100644 index 9cdd05d387a..00000000000 --- a/docs/cosmos-emulator-issues/01-invalid-session-token-error-message.md +++ /dev/null @@ -1,99 +0,0 @@ -# Linux Cosmos DB emulator returns a different error message for an invalid session token - -Tracking issue: [Azure/azure-cosmos-db-emulator-docker#291](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291) - -## Summary - -When the SDK passes a syntactically-invalid value as the `SessionToken` request header, the **Linux** Cosmos DB emulator -(`mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview`) returns the error string - -> `The session token provided 'invalidtoken' is not valid.` - -The **Windows** emulator and the real Cosmos DB service instead return - -> `The session token provided 'invalidtoken' is invalid.` - -Both responses are `400 BadRequest`, but the wording is different (`is not valid` vs. `is invalid`). Code that -asserts on the exact message text (which is what we do in our EF Core tests) fails on the Linux emulator. - -This is the failure observed in 21 of the 26 EF Core -`CosmosSessionTokensTest` failures (the `Query_uses_session_token`, -`Read_item_uses_session_token`, `Shaped_query_uses_session_token`, -`PagingQuery_uses_session_token`, and all `Add_uses_GetSessionToken` / -`Update_uses_session_token` / `Delete_uses_session_token` theory rows). - -## Stand-alone repro (no EF Core) - -`Program.csproj`: - -```xml - - - Exe - net8.0 - - - - - -``` - -`Program.cs`: - -```csharp -using System.Net.Http; -using Microsoft.Azure.Cosmos; - -const string Endpoint = "https://localhost:8081"; -const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - -var options = new CosmosClientOptions -{ - ConnectionMode = ConnectionMode.Gateway, - ConsistencyLevel = ConsistencyLevel.Session, - HttpClientFactory = () => new HttpClient(new HttpClientHandler - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }) -}; - -using var client = new CosmosClient(Endpoint, Key, options); -var db = (await client.CreateDatabaseIfNotExistsAsync("ReproDb")).Database; -var container = (await db.CreateContainerIfNotExistsAsync( - new ContainerProperties("Repro", "/pk"))).Container; - -await container.UpsertItemAsync(new { id = "1", pk = "1" }, new PartitionKey("1")); - -try -{ - await container.ReadItemAsync( - "1", new PartitionKey("1"), - new ItemRequestOptions { SessionToken = "invalidtoken" }); -} -catch (CosmosException ex) -{ - // Real Cosmos / Windows emulator: - // "The session token provided 'invalidtoken' is invalid." - // Linux emulator: - // "The session token provided 'invalidtoken' is not valid." - Console.WriteLine(ex.ResponseBody); -} -``` - -Expected output (real Cosmos / Windows emulator): - -``` -code : BadRequest -message : The session token provided 'invalidtoken' is invalid. -``` - -Actual output on the Linux emulator: - -``` -code : BadRequest -message : The session token provided 'invalidtoken' is not valid. -``` - -## Suggested fix - -Align the Linux emulator's error message with the real service so that the substring `is invalid` is preserved. diff --git a/docs/cosmos-emulator-issues/02-future-session-token-ignored.md b/docs/cosmos-emulator-issues/02-future-session-token-ignored.md deleted file mode 100644 index 3365ab7f9e4..00000000000 --- a/docs/cosmos-emulator-issues/02-future-session-token-ignored.md +++ /dev/null @@ -1,109 +0,0 @@ -# Linux Cosmos DB emulator silently accepts an unreachable (future) session token - -Tracking issue: [Azure/azure-cosmos-db-emulator-docker#291](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291) - -## Summary - -When a client passes a session token whose LSN is far in the future (one that the server can never satisfy because no -such write has occurred), the **real Cosmos DB service** and the **Windows** emulator block briefly waiting for the -session to become available and then return `404 NotFound` with sub-status `1002` and the message - -> `The read session is not available for the input session token.` - -The **Linux** emulator (`mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview`) instead **silently -returns the item with status `200 OK`**, completely ignoring the session token. - -This causes the following EF Core `CosmosSessionTokensTest+CosmosNonSharedSessionTokenTests` tests to fail because the -expected `CosmosException` is never thrown: - -- `UseSessionTokens_uses_session_tokens` -- `Read_item_session_not_found_throws_CosmosException` - -## Stand-alone repro (no EF Core) - -`Program.csproj`: - -```xml - - - Exe - net8.0 - - - - - -``` - -`Program.cs`: - -```csharp -using System.Net.Http; -using Microsoft.Azure.Cosmos; - -const string Endpoint = "https://localhost:8081"; -const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - -var options = new CosmosClientOptions -{ - ConnectionMode = ConnectionMode.Gateway, - ConsistencyLevel = ConsistencyLevel.Session, - HttpClientFactory = () => new HttpClient(new HttpClientHandler - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }) -}; - -using var client = new CosmosClient(Endpoint, Key, options); -var db = (await client.CreateDatabaseIfNotExistsAsync("ReproDb")).Database; -var container = (await db.CreateContainerIfNotExistsAsync( - new ContainerProperties("ReproFuture", "/pk"))).Container; - -// Write an item to obtain a valid session token in the form ":<...>#". -var write = await container.UpsertItemAsync( - new { id = "1", pk = "1" }, new PartitionKey("1")); -var valid = write.Headers.Session; // e.g. "0:0#5" - -// Build a token in the same range but with LSN = int.MaxValue. -var hash = valid.IndexOf('#'); -var future = valid.Substring(0, hash + 1) + int.MaxValue; -Console.WriteLine($"valid = {valid}"); -Console.WriteLine($"future = {future}"); - -try -{ - var result = await container.ReadItemAsync( - "1", new PartitionKey("1"), - new ItemRequestOptions { SessionToken = future }); - - // Linux emulator: prints "200 OK" (ignored the unsatisfiable session token). - Console.WriteLine($"Read succeeded with StatusCode={result.StatusCode}"); -} -catch (CosmosException ex) -{ - // Real Cosmos / Windows emulator: 404 with the standard "read session is not available" message. - Console.WriteLine($"Status={ex.StatusCode} SubStatus={ex.SubStatusCode}"); - Console.WriteLine(ex.ResponseBody); -} -``` - -Expected output (real Cosmos / Windows emulator): - -``` -Status=NotFound SubStatus=1002 -... The read session is not available for the input session token. ... -``` - -Actual output on the Linux emulator: - -``` -Read succeeded with StatusCode=OK -``` - -## Suggested fix - -The emulator should honor the session-token contract: when a client sends a session token whose LSN is greater than the -current max global LSN for the target partition, the request must either (a) wait for the session to become available -up to the configured timeout, or (b) return `404 NotFound` with sub-status `1002` -(`The read session is not available for the input session token.`), as the real service does. Returning `200 OK` while -ignoring the session token breaks session-consistency guarantees for clients that rely on causal reads. diff --git a/docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md b/docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md deleted file mode 100644 index b72006333f9..00000000000 --- a/docs/cosmos-emulator-issues/03-failed-write-advances-session-token.md +++ /dev/null @@ -1,125 +0,0 @@ -# Linux Cosmos DB emulator returns an extra session-token LSN across multi-context concurrency sequences - -Tracking issue: [Azure/azure-cosmos-db-emulator-docker#291](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291) - -## Summary - -While running EF Core's `CosmosSessionTokensTest+CosmosNonSharedSessionTokenTests.Optimistic_concurrency_precondition_failure_updates_session_token` -against the **Linux** emulator we observe that the set of session tokens EF Core sees across a multi-client -optimistic-concurrency sequence contains **one extra LSN value** that the same sequence does not produce against the -**Windows** emulator or the real Cosmos DB service. - -Specifically, EF Core's `CompositeSessionToken` (which accumulates every distinct session-token response value the SDK -hands it during a logical sequence) collects: - -| Environment | Tokens observed (joined) | -| --- | --- | -| Real Cosmos / Windows emulator | `0:0#51,0:0#52,0:0#0,0:0#54` | -| Linux emulator | `0:0#51,0:0#52,0:0#0,0:0#53,0:0#54` | - -Note the extra `0:0#53` slipping in between `0:0#0` and `0:0#54`. That extra LSN was returned in a response (most likely -to a *failed* write — either the 412 from the stale-ETag `Replace`, or the 412/404 from the subsequent stale-ETag -`Delete`) that should not have advanced the partition's session LSN. - -The failing assertions are: - -- `Optimistic_concurrency_precondition_failure_updates_session_token(autoTransactionBehavior: Always)` -- `Optimistic_concurrency_precondition_failure_updates_session_token(autoTransactionBehavior: Never)` - -## Stand-alone repro (no EF Core) - -The simplest deterministic repro is a two-"client" sequence: -client A creates a document, client B reads then replaces it, then client A -tries to replace using its now-stale ETag (and again to delete with the stale ETag). Capture the session token -returned in each response and look for an LSN that only appears under the Linux emulator. - -`Program.csproj`: - -```xml - - - Exe - net8.0 - - - - - -``` - -`Program.cs`: - -```csharp -using System.Net.Http; -using Microsoft.Azure.Cosmos; - -const string Endpoint = "https://localhost:8081"; -const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - -var options = new CosmosClientOptions -{ - ConnectionMode = ConnectionMode.Gateway, - ConsistencyLevel = ConsistencyLevel.Session, - HttpClientFactory = () => new HttpClient(new HttpClientHandler - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }) -}; - -using var client = new CosmosClient(Endpoint, Key, options); -var db = (await client.CreateDatabaseIfNotExistsAsync("ReproDb")).Database; -var c = (await db.CreateContainerIfNotExistsAsync(new ContainerProperties("ReproEtag", "/pk"))).Container; - -void Log(string label, string? session) => Console.WriteLine($"{label,-12} session={session}"); - -// Client A creates -var a1 = await c.CreateItemAsync(new { id = "1", pk = "1", v = 1 }, new PartitionKey("1")); -var staleEtag = a1.ETag; -Log("A create", a1.Headers.Session); - -// Client B reads and updates -var b1 = await c.ReadItemAsync("1", new PartitionKey("1")); -Log("B read", b1.Headers.Session); - -var b2 = await c.ReplaceItemAsync(new { id = "1", pk = "1", v = 2 }, "1", new PartitionKey("1"), - new ItemRequestOptions { IfMatchEtag = b1.ETag }); -Log("B replace", b2.Headers.Session); -var latestSuccessfulSession = b2.Headers.Session; - -// Client A tries to replace using its stale ETag -> 412 (FAILED write) -try -{ - await c.ReplaceItemAsync(new { id = "1", pk = "1", v = 3 }, "1", new PartitionKey("1"), - new ItemRequestOptions { IfMatchEtag = staleEtag }); -} -catch (CosmosException ex) -{ - Log("A replace*", ex.Headers?.Session); - // EXPECTED (real Cosmos / Windows emulator): same as 'B replace' (failed write does not advance LSN). - // OBSERVED (Linux emulator): may differ by 1 LSN. -} - -// Client A tries to delete using its stale ETag -> 412 (FAILED write) -try -{ - await c.DeleteItemAsync("1", new PartitionKey("1"), - new ItemRequestOptions { IfMatchEtag = staleEtag }); -} -catch (CosmosException ex) -{ - Log("A delete*", ex.Headers?.Session); -} -``` - -Expected on the real service: every session token observed after `B replace` equals `latestSuccessfulSession` until the -next successful write. Observed on the Linux emulator: at least one of the failed-write responses returns a session -token with a higher LSN than `latestSuccessfulSession`, which is then accumulated by EF Core's `CompositeSessionToken` -and causes the equality assertion to fail. - -## Suggested fix - -A failed write (412 PreconditionFailed, 404 NotFound on Delete/Replace, etc.) must not bump the partition's session -LSN, and the response header `x-ms-session-token` must reflect the last committed LSN — matching the behaviour of the -real Cosmos DB service and the Windows emulator. Otherwise, callers relying on session-token equality across -contexts to verify causality and detect concurrency outcomes (as the EF Core Cosmos provider does) see spurious -extra session tokens. diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs index cbd20b6bea1..3732b7a4d51 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosSessionTokensTest.cs @@ -598,8 +598,6 @@ protected Test2Context() } } - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/291 (Session tokens not properly tracked) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public class CosmosNonSharedSessionTokenTests(NonSharedFixture fixture) : NonSharedModelTestBase(fixture), IClassFixture { protected override ITestStoreFactory NonSharedTestStoreFactory @@ -609,7 +607,9 @@ protected override ITestStoreFactory NonSharedTestStoreFactory protected override TestStore CreateTestStore() => CosmosTestStore.Create(NonSharedStoreName, (cfg) => cfg.SessionTokenManagementMode(Cosmos.Infrastructure.SessionTokenManagementMode.SemiAutomatic)); + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/322 [ConditionalFact] + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public virtual async Task UseSessionTokens_uses_session_tokens() { var contextFactory = await InitializeNonSharedTest(); @@ -653,7 +653,9 @@ public virtual async Task ReadItem_does_not_exist_returns_null() Assert.Null(result); } + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/322 [ConditionalFact] + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public virtual async Task Read_item_session_not_found_throws_CosmosException() { var contextFactory = await InitializeNonSharedTest(); @@ -740,7 +742,9 @@ public virtual async Task Pooled_context_clears_SessionTokenStorage() Assert.True(_sessionTokenStorage.ClearCalled); } + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/319 [ConditionalTheory] + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] [InlineData(AutoTransactionBehavior.Never)] [InlineData(AutoTransactionBehavior.Always)] public virtual async Task Optimistic_concurrency_precondition_failure_updates_session_token(AutoTransactionBehavior autoTransactionBehavior)