Repository navigation
Fix raised events going to every stream, plus IsValidNumber and ReadExpectedBufferAsync false positives - #1098
Merged
Conversation
EventSlice.BuildOperations grouped RaisedEvents by stream and then handed every group all of them, so a slice raising to two streams wrote each event to both. It was worse than duplication: StreamAction.AddEvent stamps the stream identity onto the IEvent, and the instances were shared between the actions, so each later group re-tagged the events already queued for the earlier ones. The string identity branch had the same defect. The version numbering had a second one. A single stream slice that starts its stream numbers raised events from _events.Count, which is only true of the slice's own stream, but it was applied to every group. That is the shape Marten's docs recommend, a raise to this stream plus a raise to another, and it predicted the other stream's version and expected version from an unrelated event count. Only the slice's own stream is numbered now, and the store numbers the rest through QuickAppendEvents, as it already does for multi stream projections. The two branches differed only in their identity, so the append moves to one helper. A slice raising only to itself is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The group separator went into the pattern unescaped, so on de-DE, where it is ".", it matched any character and "1x234" was valid. Both parts of the pattern were optional, so "" and "-" were valid too, and $ matches before a trailing newline, so "123\n" was as well. Both separators are escaped now, the pattern has to find a digit, and it ends at \z. Everything that was a number before still is, including a bare fraction like ".5". This is fixed rather than deprecated even though nothing in the Critter Stack calls it. [Obsolete] would leave the wrong answers in place for any outside caller for the rest of 2.x, and there is no equivalent to send them to: decimal.TryParse accepts "1,00", which this rejects by design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadBytesAsync hands back a zero-padded array when the stream ends early, so ReadExpectedBufferAsync compared the padding against what it expected. A stream holding [1] matched [1, 0], and an empty stream matched [0, 0]. It now reads with Stream.ReadExactlyAsync, and its existing catch turns the EndOfStreamException into false. ReadBytesAsync itself is left as it is on 2.x, with its contract documented and pinned by a test, because Wolverine depends on it. WireProtocol reads its length prefix through it and returns quietly on a zero length, which is how a connection that closes without sending anything, like a TCP health probe, is ignored. Throwing there, or returning a short array that BitConverter then rejects, would turn each of those into an error log and a write to a closed socket in every installed Wolverine that takes a newer JasperFx. Wolverine's acknowledgement reads discard this method's result, so they do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three defects on
mainand in released 2.x packages, found by reading code and confirmed here with tests that failed first. One commit each.Raised events went to every stream
EventSlice.BuildOperationsgroupedRaisedEventsby stream, then gave every group all of them. Beyond the duplication,StreamAction.AddEventstamps the stream identity onto theIEvent, and the instances were shared between actions, so each later group re-tagged events already queued for earlier ones. Both identity branches had this.There was a second defect beneath it. A single stream slice that starts its stream numbers raised events from
_events.Count, which is only true of its own stream, but it applied that to every group. This is the shape Marten's docs recommend inTripProjectionWithEventMetadata: a raise to this stream plus a raise to another. The other stream got a version and expected version predicted from an unrelated event count.Now each stream gets only its events, only the slice's own stream is numbered, and other streams go through
QuickAppendEvents, as multi stream projections already do. Marten'smt_quick_append_eventscreates a missing stream, so a foreign stream that does not exist yet works. A slice raising only to itself is unchanged, and two tests pin that.The tests drive
RaiseSideEffectsthrough the realAggregationRunnerinto a recording batch, and read the actions after all of them are built, because a store executes them at commit. This has not run against a real store: the in-memory store has no daemon and does not enrolProjectionSideEffectCompliance, and that suite has no cross-stream fact, so nothing in this repository's gate covers it end to end.IsValidNumber: fixed, not deprecatedThe unescaped group separator was a wildcard on
de-DE, so"1x234"was valid.""and"-"were valid too, and so was"123\n", which was not in the original report:$matches before a trailing newline.Nothing in Marten, Polecat, Fisher, Weasel, Wolverine or CritterWatch calls it. It is fixed anyway because
[Obsolete]would leave the wrong answers in place for outside callers for the rest of 2.x, and there is no equivalent to point them at:decimal.TryParseaccepts"1,00", which this rejects by design. Whether to delete it in 3.0 is a separate question this does not prejudge.ReadExpectedBufferAsyncmatched paddingConfirmed: a stream holding
[1]matched[1, 0]. An empty stream also matched[0, 0]. It now usesStream.ReadExactlyAsync, and the existingcatchturns end of stream intofalse.ReadBytesAsyncstill zero-pads, deliberately. Its contract is now documented, and a test pins it. Throwing looks like the obvious fix, but Wolverine depends on the padding (src/Wolverine/Transports/Tcp/WireProtocol.cs):ReadBytesAsyncthrewlength == 0and a quiet returnLogError, then a write to the closed socketSendBatchAsyncReadExpectedBufferAsyncLine 86 is why this stays: every connection that closes without sending, such as a TCP health probe, would become an error in any installed Wolverine that takes a newer JasperFx transitively. Returning a short array has the same result, because
BitConverterrejects it. This comes from reading that code, not from running Wolverine against this branch.Lines 59 and 99 remain real hazards. Fixing them properly starts in Wolverine: move to
ReadExactlyAsyncand handle end of stream at the length prefix deliberately. After that,ReadBytesAsynccan throw or go away in 3.0.Reaching
3.0A trial merge conflicts in two files, because #1041 already changed them on
3.0:StreamExtensions.cs: keep this branch'sReadExactlyAsyncread and3.0'sexpected.AsSpan().SequenceEqual(bytes). Taking3.0's side of the hunk silently brings the bug back.StreamExtensionsTests.cs: both sides append tests, so keep both. The names do not collide.StringExtensions.csauto-merges correctly, andEventSlice.csis untouched on3.0.Verification
./build.sh Testis green in this worktree: all 14 targets, on net9.0 and net10.0 where the project targets both. EventTests 1655 passed and CoreTests 810 passed with 1 skipped. This branch adds no skips.🤖 Generated with Claude Code