From 060f22e58b26af431e5c7ab2ee2ae78a2ccfff31 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Fri, 5 Jun 2026 13:13:55 -0500 Subject: [PATCH] Fix InvalidOperationException in EventPage.CalculateCeiling on empty page When error handling is configured to skip events and every event in a full batch is skipped, the EventPage is empty (Count == 0) while Count + skippedEvents == batchSize. CalculateCeiling then called Last() on the empty page and threw "Sequence contains no elements", crashing projection rebuilds. Guard the Last() access with Count > 0 and fall back to the high water mark so the daemon keeps making progress. Fixes https://github.com/JasperFx/marten/issues/4663 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/EventTests/Projections/EventPageTests.cs | 48 ++++++++++++++++++++ src/JasperFx.Events/Projections/EventPage.cs | 7 ++- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/EventTests/Projections/EventPageTests.cs diff --git a/src/EventTests/Projections/EventPageTests.cs b/src/EventTests/Projections/EventPageTests.cs new file mode 100644 index 00000000..a0311078 --- /dev/null +++ b/src/EventTests/Projections/EventPageTests.cs @@ -0,0 +1,48 @@ +using JasperFx.Events; +using JasperFx.Events.Projections; +using Shouldly; + +namespace EventTests.Projections; + +public class EventPageTests +{ + [Fact] + public void calculate_ceiling_when_page_is_full_uses_last_sequence() + { + var page = new EventPage(0) + { + new Event(new AEvent()) { Sequence = 4 }, + new Event(new AEvent()) { Sequence = 5 } + }; + + page.CalculateCeiling(2, 1000); + + page.Ceiling.ShouldBe(5); + } + + [Fact] + public void calculate_ceiling_when_page_is_not_full_uses_high_water_mark() + { + var page = new EventPage(0) + { + new Event(new AEvent()) { Sequence = 4 } + }; + + page.CalculateCeiling(10, 1000); + + page.Ceiling.ShouldBe(1000); + } + + [Fact] + public void calculate_ceiling_when_full_batch_was_entirely_skipped_does_not_throw() + { + // Reproduces https://github.com/JasperFx/marten/issues/4663 -- every event in a + // full batch was skipped, so the page is empty. CalculateCeiling must not call + // Last() on the empty page; it should fall back to the high water mark. + var page = new EventPage(0); + + Should.NotThrow(() => page.CalculateCeiling(10, 1000, skippedEvents: 10)); + + page.Ceiling.ShouldBe(1000); + } +} diff --git a/src/JasperFx.Events/Projections/EventPage.cs b/src/JasperFx.Events/Projections/EventPage.cs index a5d2629d..0d461047 100644 --- a/src/JasperFx.Events/Projections/EventPage.cs +++ b/src/JasperFx.Events/Projections/EventPage.cs @@ -14,7 +14,12 @@ public EventPage(long floor) public void CalculateCeiling(int batchSize, long highWaterMark, int skippedEvents = 0) { - Ceiling = (Count + skippedEvents) == batchSize + // Count == 0 happens when every event in a full batch was skipped (e.g. error + // handling is configured to skip serialization/application/unknown event errors). + // There is no last event to read a sequence from, so fall back to the high water + // mark and let the daemon make progress rather than throwing on an empty page. See + // https://github.com/JasperFx/marten/issues/4663 + Ceiling = (Count + skippedEvents) == batchSize && Count > 0 ? this.Last().Sequence : highWaterMark; }