-
-
Notifications
You must be signed in to change notification settings - Fork 99
fix: true streaming reads in KurrentDB store and paged read-to-end API #568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using System.Runtime.CompilerServices; | ||
|
|
||
| namespace Eventuous; | ||
|
|
||
| public static class StoreFunctions { | ||
|
|
@@ -148,6 +150,59 @@ CancellationToken cancellationToken | |
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Reads a stream from the given position to the end, as an async enumerable. | ||
| /// Events are read in pages of <paramref name="pageSize"/> and yielded as they arrive, so the whole stream | ||
| /// is never buffered in memory. Use this instead of calling <see cref="IEventReader.ReadEvents"/> | ||
| /// with <see cref="int.MaxValue"/> as the count. | ||
| /// </summary> | ||
| /// <param name="streamName">Name of the stream to read from</param> | ||
| /// <param name="start">Stream position to start reading from</param> | ||
| /// <param name="pageSize">Number of events to read per page. It caps the amount of events a buffering | ||
| /// implementation of <see cref="IEventReader"/> holds in memory at a time.</param> | ||
| /// <param name="failIfNotFound">Set to false to complete without yielding anything when the stream isn't found, | ||
| /// instead of throwing <see cref="StreamNotFound"/>. Default is true.</param> | ||
| /// <param name="cancellationToken">Cancellation token</param> | ||
| /// <returns>An async enumerable of events retrieved from the stream</returns> | ||
| public async IAsyncEnumerable<StreamEvent> ReadStreamToEnd( | ||
| StreamName streamName, | ||
| StreamReadPosition start, | ||
| int pageSize = 500, | ||
| bool failIfNotFound = true, | ||
| [EnumeratorCancellation] CancellationToken cancellationToken = default | ||
| ) { | ||
| var position = start; | ||
|
|
||
| while (true) { | ||
| var yielded = 0; | ||
| long lastRevision = 0; | ||
|
|
||
| await using var enumerator = eventReader.ReadEvents(streamName, position, pageSize, cancellationToken).GetAsyncEnumerator(cancellationToken); | ||
|
|
||
| while (true) { | ||
| bool moved; | ||
|
|
||
| try { | ||
| moved = await enumerator.MoveNextAsync().NoContext(); | ||
| } catch (StreamNotFound) when (!failIfNotFound) { | ||
| yield break; | ||
| } | ||
|
|
||
| if (!moved) break; | ||
|
|
||
| var evt = enumerator.Current; | ||
| yielded++; | ||
| lastRevision = evt.Revision; | ||
|
|
||
| yield return evt; | ||
| } | ||
|
|
||
| if (yielded < pageSize) yield break; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a KurrentDB page contains an event that Useful? React with 👍 / 👎. |
||
|
|
||
| position = new(lastRevision + 1); | ||
| } | ||
|
Comment on lines
+200
to
+203
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Infinite loop on pagesize StoreFunctions.ReadStreamToEnd can loop forever when pageSize <= 0 because it only exits when yielded < pageSize, which is false for yielded == 0 and pageSize <= 0, so it keeps advancing position and issuing empty reads. Agent Prompt
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Reads a stream from the event store to a collection of <seealso cref="StreamEvent"/> | ||
| /// </summary> | ||
|
|
@@ -163,23 +218,10 @@ public async Task<StreamEvent[]> ReadStream( | |
| bool failIfNotFound = true, | ||
| CancellationToken cancellationToken = default | ||
| ) { | ||
| const int pageSize = 500; | ||
|
|
||
| var streamEvents = new List<StreamEvent>(); | ||
|
|
||
| var position = start; | ||
|
|
||
| try { | ||
| while (true) { | ||
| var events = await eventReader.ReadEvents(streamName, position, pageSize, failIfNotFound, cancellationToken).NoContext(); | ||
| streamEvents.AddRange(events); | ||
|
|
||
| if (events.Length < pageSize) break; | ||
|
|
||
| position = new(position.Value + events.Length); | ||
| } | ||
| } catch (StreamNotFound) when (!failIfNotFound) { | ||
| return []; | ||
| await foreach (var evt in eventReader.ReadStreamToEnd(streamName, start, failIfNotFound: failIfNotFound, cancellationToken: cancellationToken).NoContext(cancellationToken)) { | ||
| streamEvents.Add(evt); | ||
| } | ||
|
|
||
| return [.. streamEvents]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
pageSizeis zero, readers such asSqlEventStoreBasereturn an empty page, butyielded < pageSizeis false, so the outer loop repeats indefinitely and continuously queries the store until cancellation. Negative values can behave similarly for readers that treat non-positive counts as empty. Validate that this public argument is greater than zero before entering the paging loop.Useful? React with 👍 / 👎.