Class OutboxPublishCompletion
Propagation.REQUIRES_NEW).
Necessary because the hot-relay callback fires AFTER_COMMIT — at which
point the business transaction is already committed and no longer
available to participate in the markSent / markFailureAttempt
update. Spring's transaction template requires a NEW transaction here,
separate from any parent.
This is a thin wrapper around OutboxRecordRepository bulk
updates; it exists as a distinct bean so the @Transactional
proxy intercepts the call (self-invocation would bypass the proxy).
Both transition methods are guarded on status = PENDING
at the JPQL layer (R3 finding — concurrent terminal-state corruption).
A rowcount=0 from the underlying repository call indicates that
the row was already in a terminal status (SENT or FAILED); the helper
logs the no-op at DEBUG and returns that rowcount — this is the expected
outcome when a delayed hot-relay callback fires after the cold poller
has already transitioned the row (or vice versa). The rowcount is
propagated (not swallowed) so the OutboxBackend SPI's documented
1=applied / 0=no-op contract is honoured truthfully by
PgOutboxBackend (Wave A.2 Phase 0a — reviewer R1 A2/B1).
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionintmarkFailureAttempt(UUID aggregateId, UUID eventId, String lastError, int maxRetries) Persists a failure-attempt row update (retries++ + lastError) in a fresh transaction.intTransitions the row toOutboxRecord.Status.SENTin a fresh transaction.
-
Constructor Details
-
OutboxPublishCompletion
- Parameters:
repository- the JPA repository for the persistence operations
-
-
Method Details
-
markSent
@Transactional(propagation=REQUIRES_NEW) public int markSent(UUID aggregateId, UUID eventId, Instant sentAt) Transitions the row toOutboxRecord.Status.SENTin a fresh transaction. Idempotent: if the row was already terminal (e.g. transitioned toOutboxRecord.Status.FAILEDby the poller while the hot relay was still in-flight) the JPQL guard onstatus = PENDINGaffects zero rows, and the method returns normally after logging a DEBUG no-op.- Parameters:
aggregateId- compound-PK componenteventId- compound-PK componentsentAt- wall-clock instant at which the publish succeeded- Returns:
- number of rows updated (1 on applied transition; 0 when the row was already terminal — logged at DEBUG, returned to the caller)
-
markFailureAttempt
@Transactional(propagation=REQUIRES_NEW) public int markFailureAttempt(UUID aggregateId, UUID eventId, String lastError, int maxRetries) Persists a failure-attempt row update (retries++ + lastError) in a fresh transaction. The row stays PENDING; the cold poller picks it up next tick.Guarded on
status = PENDING— a delayed hot-relay failure callback that fires after the cold poller has already transitioned the row toOutboxRecord.Status.FAILED(or after a parallel hot-path SENT transition) is a no-op (R3 finding).R5 fix — the underlying repository UPDATE evaluates the retry budget atomically (see
OutboxRecordRepository.markFailureAttempt(UUID, UUID, String, int)) and conditionally transitions the row toOutboxRecord.Status.FAILEDwhenretries + 1 >= maxRetries. Callers no longer need to make the budget decision against an in-memory snapshot.- Parameters:
aggregateId- compound-PK componenteventId- compound-PK componentlastError- pre-truncated error message (caller is expected to have respectedOutboxRecord.LAST_ERROR_MAX_LENGTH)maxRetries- retry budget fromim2be.outbox.poller.max-retries; passed through to the JPQL UPDATE's CASE-WHEN clause- Returns:
- number of rows updated (1 on applied increment; 0 when the row was already terminal — logged at DEBUG, returned to the caller)
-