From fb1e8640e935d7a06037ca2826e0e89ee07edee3 Mon Sep 17 00:00:00 2001 From: Jeff Repanich Date: Thu, 17 Sep 2026 17:11:34 -0400 Subject: [PATCH] Fix SCAN Limit wire-width mismatch with the fitz broker KvScanQuery.Limit was declared ulong? and written on the wire with WriteU64 (8 bytes), but the fitz broker's SCAN decoder (read_optional_limit) only reads a u32 (4 bytes) -- matching MAX_SCAN_ITEMS and every other broker-side limit. The extra 4 bytes shifted every subsequent field (reverse, optional start_exclusive), which the broker's decoder correctly flagged as trailing data: Cntryl.Fitz.KvException: SCAN failed: Invalid request: Trailing data in SCAN payload This meant every KvDirectory.QueryAsync/ScanAllAsync call against a real broker failed -- point reads/writes were unaffected, only Scan. Root-caused by rebuilding the fitz broker from its own current main and reproducing the identical error locally, confirming this is a genuine field-width bug rather than a stale broker image. Fixes it by changing KvScanQuery.Limit to uint?/WriteU32 to match the broker's u32, updating the three (ulong) call sites in KvDirectory that constructed it, and the two tests that asserted on the old type. --- Directory.Build.props | 2 +- src/Fitz.Abstractions/Domains/Kv/KvTypes.cs | 2 +- src/Fitz.Core/Domains/Kv/KvTransaction.cs | 2 +- src/Fitz.Extensions/KvDirectory.cs | 6 +++--- test/Fitz.Extensions.Tests/KvDirectoryTests.cs | 4 ++-- test/Fitz.Testing.Tests/InMemoryKvClientTests.cs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 58d2065..0851144 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ - 1.3.0 + 1.3.1 $(PackageVersion) 1.0.0.0 $(PackageVersion) diff --git a/src/Fitz.Abstractions/Domains/Kv/KvTypes.cs b/src/Fitz.Abstractions/Domains/Kv/KvTypes.cs index 6fdefed..5682ad3 100644 --- a/src/Fitz.Abstractions/Domains/Kv/KvTypes.cs +++ b/src/Fitz.Abstractions/Domains/Kv/KvTypes.cs @@ -59,7 +59,7 @@ public sealed record KvScanResult(IReadOnlyList Pairs, bool HasMore); public sealed record KvScanQuery( ReadOnlyMemory? StartKey = null, ReadOnlyMemory? EndKey = null, - ulong? Limit = null, + uint? Limit = null, bool Reverse = false); /// A committed KV mutation notification. diff --git a/src/Fitz.Core/Domains/Kv/KvTransaction.cs b/src/Fitz.Core/Domains/Kv/KvTransaction.cs index 771e27c..c59682a 100644 --- a/src/Fitz.Core/Domains/Kv/KvTransaction.cs +++ b/src/Fitz.Core/Domains/Kv/KvTransaction.cs @@ -186,7 +186,7 @@ public async Task ScanAsync(KvScanQuery query, CancellationToken c writer.WriteU8(query.Limit.HasValue ? (byte)1 : (byte)0); if (query.Limit.HasValue) { - writer.WriteU64(query.Limit.Value); + writer.WriteU32(query.Limit.Value); } // Encode reverse flag diff --git a/src/Fitz.Extensions/KvDirectory.cs b/src/Fitz.Extensions/KvDirectory.cs index e725fa6..31a15e0 100644 --- a/src/Fitz.Extensions/KvDirectory.cs +++ b/src/Fitz.Extensions/KvDirectory.cs @@ -178,9 +178,9 @@ public async ValueTask> QueryAsync( var cursorKey = DecodeCursor(query.Cursor, fingerprint); ValidateCursorRange(cursorKey, rangeStart, rangeEnd); var scan = query.IsDescending - ? new KvScanQuery(rangeStart, cursorKey ?? rangeEnd, (ulong)(limit + 1), Reverse: true) + ? new KvScanQuery(rangeStart, cursorKey ?? rangeEnd, (uint)(limit + 1), Reverse: true) : new KvScanQuery(cursorKey is null ? rangeStart : After(cursorKey.Value.Span), rangeEnd, - (ulong)(limit + 1)); + (uint)(limit + 1)); await using var transaction = await client.BeginAsync(route, KvDurability.Async, KvMode.ReadOnly, ct) .ConfigureAwait(false); var matches = new List(limit + 1); @@ -223,7 +223,7 @@ public async ValueTask BackfillAsync( var cursorKey = DecodeCursor(cursor, fingerprint); ValidateCursorRange(cursorKey, rangeStart, rangeEnd); var scan = new KvScanQuery( - cursorKey is null ? rangeStart : After(cursorKey.Value.Span), rangeEnd, (ulong)(limit + 1)); + cursorKey is null ? rangeStart : After(cursorKey.Value.Span), rangeEnd, (uint)(limit + 1)); await using var transaction = await client.BeginAsync(route, KvDurability.Async, KvMode.ReadWrite, ct) .ConfigureAwait(false); var records = new List(limit + 1); diff --git a/test/Fitz.Extensions.Tests/KvDirectoryTests.cs b/test/Fitz.Extensions.Tests/KvDirectoryTests.cs index d684809..7728cb8 100644 --- a/test/Fitz.Extensions.Tests/KvDirectoryTests.cs +++ b/test/Fitz.Extensions.Tests/KvDirectoryTests.cs @@ -51,7 +51,7 @@ await WriteAsync(client, transaction => Directory.InsertAsync( Assert.Equal(["Alpha", "Beta"], page.Items.Select(static widget => widget.Name)); Assert.Null(page.NextCursor); Assert.All(client.Operations.Where(static operation => operation.Operation is KvTestOperation.Scan), - static operation => Assert.Equal((ulong)3, operation.ScanQuery!.Limit)); + static operation => Assert.Equal((uint)3, operation.ScanQuery!.Limit)); } [Fact] @@ -78,7 +78,7 @@ await Directory.InsertAsync(transaction, Assert.NotNull(page.NextCursor); Assert.DoesNotContain(client.Operations, static operation => operation.Operation is KvTestOperation.Get); var scan = Assert.Single(client.Operations, static operation => operation.Operation is KvTestOperation.Scan); - Assert.Equal((ulong)4, scan.ScanQuery!.Limit); + Assert.Equal((uint)4, scan.ScanQuery!.Limit); } [Fact] diff --git a/test/Fitz.Testing.Tests/InMemoryKvClientTests.cs b/test/Fitz.Testing.Tests/InMemoryKvClientTests.cs index 6dd7f76..a0b04b6 100644 --- a/test/Fitz.Testing.Tests/InMemoryKvClientTests.cs +++ b/test/Fitz.Testing.Tests/InMemoryKvClientTests.cs @@ -257,7 +257,7 @@ public async Task ShouldExposeClonedTransactionAndScanDetailsGivenDirectoryStyle Assert.Equal(KvMode.ReadOnly, begin.Mode); Assert.Equal("team\0"u8.ToArray(), scan.ScanQuery!.StartKey!.Value.ToArray()); Assert.Equal([.. "team"u8, 1], scan.ScanQuery.EndKey!.Value.ToArray()); - Assert.Equal(25UL, scan.ScanQuery.Limit); + Assert.Equal(25U, scan.ScanQuery.Limit); Assert.True(scan.ScanQuery.Reverse); }