Skip to content

Failed event publications that are skipped or keep failing block resubmission of all others #1906

Open

Description

@AdzerKI

FailedEventPublications.resubmit(…) reads at most min(batchSize, maxInFlight - resubmitted) failed publications, ordered by publication date, and only then applies ResubmissionOptions.getFilter(). So the read window always holds the oldest failed publications:

  • a publication that fails on every attempt is read again on every run and stays at the head of the queue for good,
  • a publication the filter skips still takes a slot in the window. We use the filter for a retry backoff (last attempt plus a pause growing with getCompletionAttempts()), so recently retried publications are skipped until their pause is over.

Once batchSize of these pile up, no other failed publication gets resubmitted: every run reads the same rows, skips or fails them again, and stops. A larger batch size only moves the threshold. We ran into this while adding the backoff filter on top of a scheduled resubmit(…).

Reading the least recently attempted publications first fixes it: order by LAST_RESUBMISSION_DATE, falling back to the publication date for publications that were never resubmitted. A publication that has just been resubmitted and failed again goes to the end of the queue, so every failed publication gets its turn, and a skipped one only holds its slot until it's due. The JDBC repository already initializes LAST_RESUBMISSION_DATE with the publication date, JPA, MongoDB and Neo4j leave it null until the first resubmission, hence the fallback.

Seen on 2.1.1, main has the same queries. PR follows.

Activity

  1. odrotbohm commented on Oct 1, 2026

    @odrotbohm
    Member

    I am not quite sure that we can reorder the resubmissions as you suggest. Let's say the publications target the same entity and the events can only be reacted to in order (e.g., an “order cancelled” can only be reacted to after an “order placed” has successfully been processed. In this case, it doesn't make sense to proceed with subsequent events if the processing of the previous events has failed.

    For 2.2, we're introducing an additional “abandoned” state, which event publications will enter after a configurable amount of resubmissions or programmatic selection. Would that help mitigate the problem you face? Furthermore, couldn't you already skip offending publications by setting up a ResubmissionOptions instance accordingly? I wonder if we could add a flag to opt-in to the lookup algorithm you prefer on ResubmissionOptions to cover scenarios that can be more relaxed on the overall ordering.

  2. self-assigned this
    on Oct 1, 2026
  3. char-yb commented on Oct 6, 2026

    @char-yb
    Contributor

    I hit the same problem on 2.1.1 from a slightly different direction, so here's a data point.

    On 2.1.1 we tried to cap retries with withFilter(it -> it.getCompletionAttempts() < 60). It doesn't work, for the reason described above: the filter runs after findFailedPublications(…) has already applied the limit (min(batchSize, maxInFlight - resubmitted), oldest first). Once 500 publications (our batch size) have hit the cap, they fill the window on every run and nothing behind them gets resubmitted, even after the broker is back. With a one-minute schedule that's a broker outage of about an hour with more than 500 failed publications. So skipping through ResubmissionOptions made things worse for us, and we ended up dropping the cap and relying on idempotent consumers.

    I had a look at the abandonment support on main, and for that case it does solve the problem: the policy is applied when the publication fails, and findFailedPublications(…) only matches FAILED, so abandoned publications never take a slot in the window. It doesn't help with the backoff filter from the issue description, though. A publication that is skipped but not abandoned still occupies its slot, so that part would still need the opt-in ordering.

    One thing I'm not sure is covered: failures that never reach a listener invocation don't seem to go through the AbandonPolicy. TransactionalEventListeners.invoke(…) evaluates publication.getApplicationEvent() before calling processEvent(…), so a payload that can't be deserialized throws into the catch block of processPublications(…) (the same spot as #1904) and the publication stays RESUBMITTED. If the staleness monitor flips it back later, that goes through EventPublicationRepository.markFailed(…) directly, so the policy isn't consulted there either. The reference docs list "a payload that can never be deserialized" as a typical case for abandoning, so unless I'm missing something, that case can't actually end up abandoned right now. I can open a separate issue for it if you prefer.

    On ordering, I agree it matters for events targeting the same entity. Two things I noticed, though. The blocking is global today, so a publication for one aggregate holds back retries for unrelated ones. And the reference docs already point out that later events might overtake earlier ones during resubmission unless serialize-externalization is enabled, so for externalized events strict ordering isn't guaranteed anyway. An opt-in flag on ResubmissionOptions sounds like a reasonable compromise to me.

  4. AdzerKI commented on Oct 9, 2026

    @AdzerKI
    ContributorAuthor

    Sorry for the late reply. The abandoned state covers publications that keep failing: they leave FAILED and stop taking slots in the window. It does not cover the backoff filter: a publication the filter skips is still FAILED and is still read into the window. Skipping offending publications through ResubmissionOptions hits the same limit, because the filter only sees the rows that were already read (char-yb's cap above is exactly that case).

    An opt-in on ResubmissionOptions works for us. I'll rework #1907 that way: the current order stays the default, and an option along the lines of withLeastRecentlyAttemptedFirst() makes the repositories order by the last resubmission date, falling back to the publication date.

  5. added a commit that references this issue on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions