Skip to content

[#4797] Fall back to the event identifier when a sequencing policy resolves nothing - #4818

Open
schananas wants to merge 1 commit into
mainfrom
bug/4797/sequence-identifier-optional-fallback
Open

[#4797] Fall back to the event identifier when a sequencing policy resolves nothing#4818
schananas wants to merge 1 commit into
mainfrom
bug/4797/sequence-identifier-optional-fallback

Conversation

@schananas

@schananas schananas commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #4797

What changed

SequencingPolicy.sequenceIdentifierFor documents an empty result as normal, but SimpleEventHandlingComponent unwrapped it with Optional.get(), under a @SuppressWarnings("OptionalGetWithoutIsPresent"). Both call sites now fall back to the event identifier.

Two shipped policies legitimately answer empty, so they threw NoSuchElementException on every event and the processor delivered nothing at all, for ever: NoOpSequencingPolicy, and SequentialPerAggregatePolicy on any store that does not set the legacy aggregate identifier.

The old behaviour was not silent: DefaultWorkPackageEventFilter catches, PropagatingErrorHandler rethrows, and WorkPackage.canHandle:321 logs "Error while detecting whether event can be handled in Work Package ... Aborting Work Package" with the Optional.get stack trace, then aborts. The permanent stall is the defect; the silence is not.

Why this fallback

FullConcurrencyPolicy already returns Optional.of(message.identifier()). A per-event identifier is how the framework encodes "no sequencing", so falling back to it makes an empty Optional mean the same thing as the policy that explicitly asks for no sequencing.

A warning when the fallback fires

SimpleEventHandlingComponent now warns once per component, naming the policy class, when a policy resolves no identifier and the fallback is used. Without it, explicitly configuring SequentialPerAggregatePolicy on a DCB store goes from a loud abort to silently getting one sequence per event -- the user asked for per-aggregate ordering and gets none.

NoOpSequencingPolicy is exempt: an empty answer there is the user's deliberate declaration that they want no sequencing, and a warning that fires on correct configuration gets muted wholesale, burying the case it exists for. Recognising it by type follows MessagingConfigurationDefaults:345, which already does !(commandSequencingPolicy instanceof NoOpSequencingPolicy) for the same reason, and NoOpSequencingPolicy's own Javadoc sanctions it: "Infrastructure components may decide upon this sequencing policy being present bypassing sequencing infrastructure at all."

The wired default never returns empty, so it never warns -- asserted by a test, not just reasoned.

Tests

PolicyCannotDetermineAnIdentifier covers all three branches, including a plain-EventHandler component, the shape a real annotated projection takes. FallbackToEventIdentifierIsLogged covers the warning: fires once for SequentialPerAggregatePolicy with no aggregate identifier, silent for NoOpSequencingPolicy, silent for the wired default.

Against the true pre-fix production file: 6 errors, all NoSuchElementException: No value present. The default-policy test correctly stays green in both directions, as a regression guard should.

One detail worth knowing if anyone tries to shrink the diff: the orElse to orElseGet change is load-bearing on its own. Optional.orElse evaluates its argument eagerly, so the old code threw even when a nested component had already answered.

Targeted run, no failures:

Class tests
SimpleEventHandlingComponentSequencingPolicyTest 13
SimpleEventHandlingComponentTest 7
SegmentMatcherTest 3
SequentialPerAggregatePolicyTest 3

No existing test needed updating; nothing had encoded the old throwing behaviour.

For the reviewer

The behaviour change worth a second opinion: per-event identifiers mean "no sequencing", so events under NoOpSequencingPolicy may now be handled concurrently and spread across segments, where before they were handled not at all. A total read-side stall is not a contract worth preserving, but the chosen default is a judgement call.

This does not fix the separate problem that SequentialPerAggregatePolicy resolves nothing on a DCB store. With this change it degrades quietly to per-event identifiers instead of throwing. That degradation is #4803.

Merge note

bug/4803/default-sequencing-policy-dcb-degradation-docs touches the same two files in different regions. Both derive from the same base blobs off origin/main, so no conflict is expected, but merge one before rebasing the other.

@schananas
schananas requested a review from a team as a code owner July 29, 2026 15:36
@schananas
schananas requested review from hatzlj, laura-devriendt-lemon and zambrovski and removed request for a team July 29, 2026 15:36
@schananas schananas self-assigned this Jul 29, 2026
@schananas
schananas force-pushed the bug/4797/sequence-identifier-optional-fallback branch from 967be50 to eb38968 Compare July 29, 2026 17:46
…solves nothing

`SequencingPolicy.sequenceIdentifierFor` documents an empty result as normal, but
`SimpleEventHandlingComponent` unwrapped it with `Optional.get()`. `NoOpSequencingPolicy`, and
`SequentialPerAggregatePolicy` on any store that does not set the legacy aggregate identifier,
therefore threw `NoSuchElementException` on every event and the processor delivered nothing at all,
for ever, with no configuration error to point at it.

Both call sites now fall back to the event identifier, which is what `SegmentMatcher` already does,
so "no sequencing" means full concurrency instead of a total read-side stall.

Trading the abort for a fallback trades a loud failure for a silent downgrade: a projection that
asked for per-aggregate ordering now gets none, and nothing said so. The component therefore warns
on the first fallback, naming the policy and the component. Once per component, not once per event,
as this sits on the per-event path and an event-rate warning would be its own defect. The wired
default, `HierarchicalSequencingPolicy(SequentialPerAggregatePolicy, SequentialPolicy)`, always
resolves through its secondary policy, so the warning cannot fire for the default configuration.

`NoOpSequencingPolicy` is exempt from the warning. An empty result there is the user explicitly
declaring that no sequencing is wanted, not a degradation, and a warning that fires on correct
configuration is the kind that gets filtered out wholesale, burying the case it exists for.
`MessagingConfigurationDefaults` already recognises this policy by type for the same reason, so the
exception is carved at the log site rather than by widening the `SequencingPolicy` interface for it.

Fixes #4797
@schananas
schananas force-pushed the bug/4797/sequence-identifier-optional-fallback branch from eb38968 to 97db32a Compare July 29, 2026 17:55
@MateuszNaKodach
MateuszNaKodach self-requested a review July 31, 2026 12:46

@MateuszNaKodach MateuszNaKodach left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good finding! Wow. I think we can merge that :)

@MateuszNaKodach MateuszNaKodach added Type: Bug Use to signal issues that describe a bug within the system. Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. labels Jul 31, 2026
@MateuszNaKodach MateuszNaKodach added this to the Release 5.4.0 milestone Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. Type: Bug Use to signal issues that describe a bug within the system.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Choosing NoOpSequencingPolicy, or SequentialPerAggregatePolicy on a DCB store, makes the processor deliver nothing at all

2 participants