Skip to content

Failed client-Java listener delivery is retried, not lost - #6832

Open
TIVMOF wants to merge 14 commits into
eclipse-dirigible:masterfrom
TIVMOF:fix/no-retry
Open

Failed client-Java listener delivery is retried, not lost#6832
TIVMOF wants to merge 14 commits into
eclipse-dirigible:masterfrom
TIVMOF:fix/no-retry

Conversation

@TIVMOF

@TIVMOF TIVMOF commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

This PR:

Problem

The client-Java listener path — which carries all intent glue — had no retry, no redelivery and no dead letter. ListenerClassConsumer.dispatch caught the handler's exception and never rethrew it, so the broker saw every failed delivery as a success, acknowledged the message and dropped the event permanently.

That also stranded handlers whose correctness depends on a second delivery. Posting.java.template documents and implements an idempotent-and-resumable contract: a redelivery of a half-written post clears the partial rows and rebuilds the set. The code is correct, but its one precondition was redelivery, so the repair could never run — a journal header with 2 of 5 lines stayed unbalanced forever.

Change

Rethrow after reporting in ListenerClassConsumer.dispatch. This is the half that actually fixes it: dispatcher.onError(...) still runs first, then the failure leaves the listener where the broker can act on it.

Bound the retries by configuring a redelivery policy on the subscription. Worth being precise about what this does — the rethrow alone already yields redelivery under ActiveMQ's built-in defaults; this makes the Java path use the same budget as the JavaScript path (1s initial, 5s delay, exponential backoff, max 3) rather than a different one.

Rather than copy that policy into engine-java, it moves onto ActiveMQConnectionArtifactsFactory — the collaborator both paths already inject — with ListenerManager delegating. The retry budget is now one number in one place, and engine-java never names an ActiveMQ type.

What this deliberately does not change

The session stays AUTO_ACKNOWLEDGE. Both paths call the same createSession(false, AUTO_ACKNOWLEDGE)ListenerManager:83 and ListenerClassConsumer:269 — so the session mode cannot be what separated the working path from the broken one. For an asynchronous listener, ActiveMQ acknowledges only after onMessage returns, which is exactly why the JS path retries on the same session. Making sessions transacted would have changed error and threading semantics for every listener, including the ones already working, for no benefit.

The text-read failure path still returns and drops the message. A body that can't be read is likely poison and arguably belongs in the DLQ, but that's out of scope here.

Testing

Three new tests, verified red-first: with both changes stripped they fail with Expected java.lang.Exception to be thrown, but nothing was thrown — precisely the swallow — while the pre-existing tests kept passing. They assert the failure escapes onMessage, that the original exception survives as the cause rather than being flattened to a message, that onError still runs for the failed attempt, and that the subscription configures its redelivery policy.

Green after restore: ListenerClassConsumerTest 5/5, plus ListenersManagerTest and AsynchronousMessageListenerTest confirming the ListenerManager refactor left the JavaScript path intact.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client-Java listener path has no retry and no dead-letter - and makes the posting engine's resumability contract unreachable

2 participants