Interface OutboxRecordRepository
- All Superinterfaces:
org.springframework.data.repository.CrudRepository<OutboxRecord,,OutboxRecord.PK> org.springframework.data.jpa.repository.JpaRepository<OutboxRecord,,OutboxRecord.PK> org.springframework.data.repository.ListCrudRepository<OutboxRecord,,OutboxRecord.PK> org.springframework.data.repository.ListPagingAndSortingRepository<OutboxRecord,,OutboxRecord.PK> org.springframework.data.repository.PagingAndSortingRepository<OutboxRecord,,OutboxRecord.PK> org.springframework.data.repository.query.QueryByExampleExecutor<OutboxRecord>,org.springframework.data.repository.Repository<OutboxRecord,OutboxRecord.PK>
OutboxRecord.
The default JpaRepository surface covers CRUD against the
compound PK OutboxRecord.PK; the additional poller-specific query
methods declared here drive the PENDING → SENT transition
implemented in OutboxPollerWorker (ADR-0014 D-7).
All non-bulk methods are read-only by default; the Modifying
methods (markSent(java.util.UUID, java.util.UUID, java.time.Instant), markFailureAttempt(java.util.UUID, java.util.UUID, java.lang.String, int)) MUST be invoked
inside an active transaction — they're configured to clear the persistence
context via flushAutomatically=true, clearAutomatically=true so
downstream reads see the up-to-date row state.
-
Method Summary
Modifier and TypeMethodDescriptionlongcountByStatus(OutboxRecord.Status status) Pending-total gauge support.org.springframework.data.domain.Page<OutboxRecord>findByStatusOrderByCreatedAtAsc(OutboxRecord.Status status, org.springframework.data.domain.Pageable pageable) Pageable PENDING-row fetch for the poller's cold-path sweep.Backlog-age gauge support.intmarkFailureAttempt(UUID aggregateId, UUID eventId, String lastError, int maxRetries) Atomic retry-bump on publish failure.intAtomic PENDING → SENT transition for a single row keyed by the compound PK.Methods inherited from interface org.springframework.data.repository.CrudRepository
count, delete, deleteAll, deleteAll, deleteAllById, deleteById, existsById, findById, saveMethods inherited from interface org.springframework.data.jpa.repository.JpaRepository
deleteAllByIdInBatch, deleteAllInBatch, deleteAllInBatch, deleteInBatch, findAll, findAll, flush, getById, getOne, getReferenceById, saveAllAndFlush, saveAndFlushMethods inherited from interface org.springframework.data.repository.ListCrudRepository
findAll, findAllById, saveAllMethods inherited from interface org.springframework.data.repository.ListPagingAndSortingRepository
findAllMethods inherited from interface org.springframework.data.repository.PagingAndSortingRepository
findAllMethods inherited from interface org.springframework.data.repository.query.QueryByExampleExecutor
count, exists, findAll, findBy, findOne
-
Method Details
-
findByStatusOrderByCreatedAtAsc
org.springframework.data.domain.Page<OutboxRecord> findByStatusOrderByCreatedAtAsc(OutboxRecord.Status status, org.springframework.data.domain.Pageable pageable) Pageable PENDING-row fetch for the poller's cold-path sweep. Ordering bycreatedAt ASCensures FIFO replay behaviour, which combined with the(status, created_at)compound index makes the scan O(N) in batch size, not table size.- Parameters:
status- the target status (typicallyOutboxRecord.Status.PENDING)pageable- bounded page; the poller passes a page ofOutboxProperties.poller.batch-sizeper tick- Returns:
- a page of rows in FIFO creation order
-
findOldestCreatedAtByStatus
@Query("SELECT MIN(o.createdAt) FROM OutboxRecord o WHERE o.status = :status") Optional<Instant> findOldestCreatedAtByStatus(@Param("status") OutboxRecord.Status status) Backlog-age gauge support. Returns thecreatedAtof the oldest row in the given status (typically PENDING); the metrics binder subtracts this fromnowto exposeim2be_outbox_backlog_age_seconds.- Parameters:
status- the target status- Returns:
- the oldest
createdAt, or empty when no rows match
-
countByStatus
Pending-total gauge support. Total row count for a given status — sourced via JPQL count so the query planner uses the(status, created_at)index for an index-only scan.- Parameters:
status- the target status- Returns:
- total number of rows in that status
-
markSent
@Modifying(flushAutomatically=true, clearAutomatically=true) @Query("UPDATE OutboxRecord o SET o.status = com.aim2be.platform.outbox.OutboxRecord.Status.SENT, o.sentAt = :sentAt, o.lastError = NULL WHERE o.aggregateId = :aggregateId AND o.eventId = :eventId AND o.status = com.aim2be.platform.outbox.OutboxRecord.Status.PENDING") int markSent(@Param("aggregateId") UUID aggregateId, @Param("eventId") UUID eventId, @Param("sentAt") Instant sentAt) Atomic PENDING → SENT transition for a single row keyed by the compound PK. ClearslastErroras part of the transition (a successful publish supersedes any prior failure context).Invoked by both the hot AFTER_COMMIT relay (via
OutboxPublisher.tryHotPublish) and the cold poller (viaOutboxPollerWorker). TheWHERE … status = PENDINGguard makes the transition idempotent under concurrent paths — a delayed hot-relay callback that fires AFTER the cold poller has already transitioned the row toOutboxRecord.Status.FAILED(or vice versa) MUST NOT corrupt the terminal state. Therowcount=0return value signals the no-op to the caller (R3 finding — concurrent terminal-state corruption).R4 fix — the target status is hard-coded to
OutboxRecord.Status.SENTinside the JPQL. The previousnewStatusparameter exposed a footgun via the public repository API: a caller passingOutboxRecord.Status.FAILEDwould persist a non-nullsentAton a never-delivered row. Distinct lifecycle transitions belong on distinct repository methods.- 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 in a terminal status — caller logs at DEBUG)
-
markFailureAttempt
@Modifying(flushAutomatically=true, clearAutomatically=true) @Query("UPDATE OutboxRecord o SET o.retries = o.retries + 1, o.lastError = :lastError, o.status = CASE WHEN o.retries + 1 >= :maxRetries THEN com.aim2be.platform.outbox.OutboxRecord.Status.FAILED ELSE com.aim2be.platform.outbox.OutboxRecord.Status.PENDING END WHERE o.aggregateId = :aggregateId AND o.eventId = :eventId AND o.status = com.aim2be.platform.outbox.OutboxRecord.Status.PENDING") int markFailureAttempt(@Param("aggregateId") UUID aggregateId, @Param("eventId") UUID eventId, @Param("lastError") String lastError, @Param("maxRetries") int maxRetries) Atomic retry-bump on publish failure. Increments the row'sretriescounter, stores the (truncated) error message, AND conditionally transitions the row toOutboxRecord.Status.FAILEDwhen the post-increment retries count reaches the configuredmaxRetriesbudget — all in a single JPQL UPDATE.R5 fix — the previous shape (Java-side budget check on the in-memory batch snapshot) raced with concurrent hot-relay failures that could push DB retries past
maxRetriesbetween the poller's batch fetch and the failure-outcome write. Evaluating the budget INSIDE the UPDATE statement uses the database's CURRENT retries value at write time, eliminating the snapshot race.Guarded on
status = PENDINGso a late hot-relay failure callback that fires AFTER the cold poller has already transitioned the row toOutboxRecord.Status.FAILED(or a parallel hot path has already transitioned it toOutboxRecord.Status.SENT) cannot mutate the terminal row (R3 finding — concurrent terminal-state corruption). Therowcount=0return value signals the no-op to the caller.- Parameters:
aggregateId- compound-PK componenteventId- compound-PK componentlastError- stringified exception summary; the caller is expected to truncate toOutboxRecord.LAST_ERROR_MAX_LENGTHcharacters before invocationmaxRetries- retry budget fromim2be.outbox.poller.max-retries; whenretries + 1 >= maxRetriesthe row also transitions toOutboxRecord.Status.FAILEDin the same UPDATE statement- Returns:
- number of rows updated (1 on applied increment; 0 when the row was already in a terminal status — caller logs at DEBUG)
-