Repository navigation
Harden StreamPage primitives and make cursor wire formats culture-invariant - #10
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
One or more issues must be addressed before approval.
Review effort: Lite
Findings: 2
Open (4)
What changed in this PR
Hardens streaming page lifecycle management and makes cursor serialization culture-invariant.
Changes:
- Restricts page construction to primed/internal factories and renames entry enumeration APIs.
- Improves disposal, fault propagation, and batch key-order validation.
- Uses invariant culture for temporal and end-cursor formats.
| File | Description |
|---|---|
| src/GreenDonut/test/GreenDonut.Data.Tests/StreamPageTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/StreamPageCursorTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/StreamBatchPumpTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/ScriptedAsyncSourceTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/ScriptedAsyncSource.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/PageTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/OrderedDisposalTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/TimeOnlyCursorKeySerializerTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/DateTimeOffsetCursorKeySerializerTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/DateTimeCursorKeySerializerTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/Serializers/DateOnlyCursorKeySerializerTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/CursorParserTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/StreamRelativeCursorTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/StreamPagingTotalCountTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/StreamPagingHelperTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/StreamPagingFaultTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/SnapshotExtensions.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/EndCursorTests.cs | Updated as part of this pull request. |
| src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/CursorInteropTests.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Internal/ThrowHelper.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Extensions/StreamPageCursorExtensions.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutStreamPageExtensions.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutPageExtensions.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/TimeOnlyCursorKeySerializer.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/DateTimeOffsetCursorKeySerializer.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/DateTimeCursorKeySerializer.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/DateOnlyCursorKeySerializer.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data/Cursors/CursorFormatter.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/ValueCursorStreamPage.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/StreamPage.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/ThrowHelper.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/StreamPageSourceBase.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/StreamPagePump.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/StreamPageBuffer.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/StreamBatchPump.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/OrderedDisposal.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/IStreamPageSource.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/Internal/ElementProjectingSource.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.Primitives/ElementCursorStreamPage.cs | Updated as part of this pull request. |
| src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/StreamPagingQueryableExtensions.cs | Updated as part of this pull request. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| _fault = ExceptionDispatchInfo.Capture(ex); | ||
| await ReleaseCoreAsync().ConfigureAwait(false); | ||
|
|
||
| try | ||
| { | ||
| await ReleaseCoreAsync().ConfigureAwait(false); |
There was a problem hiding this comment.
Agree. The fault is checked before a staged row is returned, so a sibling's next pull rethrows the original exception instead of yielding buffered rows; regression with a queued sibling row added. Follow-up PR (task hc-fork-1-dno.13).
| if (channel.Completed && !channel.Abandoned) | ||
| { | ||
| // the key's run already completed (a later key was seen, or the source reached its | ||
| // end) and it was not abandoned, so this row is a source ordering violation: it would | ||
| // land in a queue no page will ever read again. Fault exactly like a mid-stream source | ||
| // exception so the source and the lifetime are released and every later pull, for any | ||
| // key, rethrows. A disposal failure while releasing is attached to this fault instead | ||
| // of replacing it. | ||
| var fault = ThrowHelper.StreamBatchPump_SourceNotGroupedByKey(row.Key); |
There was a problem hiding this comment.
Agree. The unrequested-key violation goes through the same fault-and-release path as the ordering violation (fault recorded, source and lifetime released, every later pull rethrows). Follow-up PR (task hc-fork-1-dno.13).
| try | ||
| { | ||
| await enumerator.DisposeAsync().ConfigureAwait(false); | ||
| } | ||
| catch (Exception disposeException) | ||
| { | ||
| // the enumerator's own disposal failure is what the caller must observe; the | ||
| // lifetime is still released, with a failure releasing it attached instead of | ||
| // replacing the original failure. | ||
| try | ||
| { | ||
| await DisposeLifetimeAsync(lifetime).ConfigureAwait(false); | ||
| } | ||
| catch (Exception lifetimeException) | ||
| { | ||
| OrderedDisposal.Attach(disposeException, lifetimeException); |
There was a problem hiding this comment.
Agree. The lifetime release moves into a cleanup path that also covers the fallback CountAsync; the count exception stays primary with disposal failures attached. Follow-up PR (task hc-fork-1-o47.14).
| private static async ValueTask DrainAsync<TValue>( | ||
| StreamPage<TValue> page, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| await foreach (var _ in page.GetEntriesAsync(cancellationToken).ConfigureAwait(false)) | ||
| { | ||
| // draining only for its buffering side effect; the entries themselves are unused here. | ||
| } | ||
| } |
There was a problem hiding this comment.
Agree. DrainAsync catches a selector/drain failure, disposes the drained page so its key releases its count, attaches cleanup failures and rethrows the original. Follow-up PR (task hc-fork-1-dno.13).
ccbe696 to
6ef2248
Compare
…s for stream pumps
…-SE round-trip test
…ge factory surface
6ef2248 to
2d60f02
Compare
…ulture round-trip test signature


This hardens the StreamPage primitives and closes the culture bugs found in review.
StreamPage<T>no longer carries lifecycle internals: the source owns priming, pages can only be built already primed (private constructors,CreatePrimedAsyncfor single pages, and an internalCreateForBatchused only by the batch pump, guarded by a reflection test), andEnumerateEntriesAsyncis renamed toGetEntriesAsync. Every failure path now releases the source and the lifetime without masking the original exception. Each disposal runs in its own try, and disposal errors are attached to the original fault instead of replacing it. The batch pump throws when a key reappears after its page completed, instead of silently dropping those rows. Cursors are now culture-invariant: end cursors and the date and time key serializers format and parse with the invariant culture, so a cursor written underth-THno longer decodes to the year 2569. That serializer bug also exists onmain. Invariant-culture output is byte-identical, so existing cursors keep working.Closes the Copilot threads on #1 (inline exceptions in StreamPageBuffer.cs:73 and StreamPage.cs:157), #2 (
maxCursors: 0still returning a cursor, GreenDonutStreamPageExtensions.cs:129), #3 (lifetime skipped when source disposal throws, StreamBatchPump.cs:87), #6 (disposal masking the stream fault, StreamBatchPump.cs:169 and StreamPageBuffer.cs:218) and #8 (no test for the priming read throwing, StreamBatchPump.cs:100). Known follow-up: one batch-pump test lost its mid-drain assertion when the test-only accessor was removed.Stack: sits on #8 (
mst/primitives-batch-priming, which now also carries #9).