diff --git a/CHANGELOG.md b/CHANGELOG.md index e566e53..74f1020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ never breaks for consumers. `test/Fitz.PackageConsumer` asserts this on every CI ## [Unreleased] +## [1.4.2] - 2026-09-21 + +### Fixed + +- **`Cntryl.Fitz.Abstractions`:** expose `FitzErrorCodes.StreamConcurrencyConflict` (2001) and + `FitzErrorCodes.StreamSessionAlreadyActive` (2002), so callers can classify the broker's + structured stream write-contention responses without matching error text. + ## [1.4.1] - 2026-09-21 ### Added diff --git a/Directory.Build.props b/Directory.Build.props index d52ec64..a6ad2e9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ - 1.4.1 + 1.4.2 $(PackageVersion) 1.0.0.0 $(PackageVersion) @@ -26,7 +26,7 @@ portable README.md fitz distributed-systems kv queue rpc lease notice stream schedule - Adds bounded read-only primary-key paging for Cntryl.Fitz.Extensions directories. See CHANGELOG.md. + Exposes named stream write-contention error codes for broker codes 2001 and 2002. See CHANGELOG.md. $(MSBuildThisFileDirectory)artifacts\packages diff --git a/docs/guide.md b/docs/guide.md index 6172761..a1a4b16 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -125,9 +125,10 @@ catch (RpcException ex) when (ex.DomainCode == FitzErrorCodes.RpcTimeout) } ``` -`FitzErrorCodes` lists every code the broker defines. Transport and lifecycle problems surface -as `ConnectionException`, `AuthenticationException`, `RequestTimeoutException`, -`RequestQueueFullException`, or `ProtocolException`. +`FitzErrorCodes` exposes named constants for the broker codes the SDK classifies. Other +structured domain codes remain available on an exception's `DomainCode`; never match error text. +Transport and lifecycle problems surface as `ConnectionException`, `AuthenticationException`, +`RequestTimeoutException`, `RequestQueueFullException`, or `ProtocolException`. Two exceptions sit outside that hierarchy and derive from `Exception` directly: `SubscriptionBackpressureException` and `AsyncHandlerOverflowException`. They are declared in @@ -313,8 +314,11 @@ await session.CommitAsync(ct); ``` `AppendAsync` takes the offset you expect to write at; a mismatch is an optimistic-concurrency -failure, reported as `StreamException` with `DomainCode` 2001. Classify on that code, never on -the message. +failure, reported as `StreamException` with `DomainCode == +FitzErrorCodes.StreamConcurrencyConflict`. `BeginAsync` reports +`FitzErrorCodes.StreamSessionAlreadyActive` when an append session currently owns the resource +stream. These are distinct structured write-contention conditions; classify on the +code, never on the message. Use `ReadPageAsync` when you need to see what the broker withheld: a `StreamReadPage` contains `StreamReadItem`s whose `Kind` distinguishes delivered records from filtered ones, so gaps in diff --git a/src/Fitz.Abstractions/FitzErrorCodes.cs b/src/Fitz.Abstractions/FitzErrorCodes.cs index af64482..21df8bd 100644 --- a/src/Fitz.Abstractions/FitzErrorCodes.cs +++ b/src/Fitz.Abstractions/FitzErrorCodes.cs @@ -30,6 +30,17 @@ public static class FitzErrorCodes /// public const uint KvSubscriptionLimit = 1013; + /// + /// An append operation found that the stream's committed position no longer matches its + /// expected position. + /// + public const uint StreamConcurrencyConflict = 2001; + + /// + /// An append session is already active for the resource stream. + /// + public const uint StreamSessionAlreadyActive = 2002; + /// /// The stream subscription selector was malformed or unsupported. /// diff --git a/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs b/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs index 8e56214..b4277ae 100644 --- a/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs +++ b/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs @@ -5,10 +5,20 @@ namespace Cntryl.Fitz.Core.Tests.Unit; public sealed class StreamErrorEnvelopeTests { + [Fact] + public void ShouldMatchWireProtocolGivenStreamWriteContentionCodesWhenValidated() + { + // Arrange + // Act + // Assert + Assert.Equal(2001u, FitzErrorCodes.StreamConcurrencyConflict); + Assert.Equal(2002u, FitzErrorCodes.StreamSessionAlreadyActive); + } + [Theory] - [InlineData("APPEND", 2001u, "unrelated wording")] - [InlineData("COMMIT", 2001u, "unrelated wording")] - [InlineData("APPEND", 2002u, "concurrency conflict")] + [InlineData("APPEND", FitzErrorCodes.StreamConcurrencyConflict, "unrelated wording")] + [InlineData("COMMIT", FitzErrorCodes.StreamConcurrencyConflict, "unrelated wording")] + [InlineData("BEGIN", FitzErrorCodes.StreamSessionAlreadyActive, "unrelated wording")] [InlineData("COMMIT", 2012u, "backend unavailable")] [InlineData("BEGIN", 2003u, "session unavailable")] [InlineData("ROLLBACK", 2003u, "session unavailable")] diff --git a/test/Fitz.PackageConsumer/Program.cs b/test/Fitz.PackageConsumer/Program.cs index 4d6fe73..1303e7d 100644 --- a/test/Fitz.PackageConsumer/Program.cs +++ b/test/Fitz.PackageConsumer/Program.cs @@ -56,6 +56,16 @@ throw new InvalidOperationException("RPC error code constants must cover the canonical 6001-6013 range."); } +uint[] streamWriteContentionCodes = +[ + FitzErrorCodes.StreamConcurrencyConflict, + FitzErrorCodes.StreamSessionAlreadyActive, +]; +if (!streamWriteContentionCodes.SequenceEqual(Enumerable.Range(2001, 2).Select(static code => (uint)code))) +{ + throw new InvalidOperationException("Stream write-contention error code constants must cover the canonical 2001-2002 range."); +} + static async Task CompilePreviewApiAsync( INoticeClient notice,