Interface IdempotencyStore

All Known Implementing Classes:
InMemoryIdempotencyStore, JdbcIdempotencyStore

public interface IdempotencyStore
Remembers which messages have already been handled, so handling one twice does not happen twice.

Every broker worth using delivers at least once, which means duplicates are not an error condition to be avoided but a normal event to be absorbed. A message is redelivered whenever a consumer dies between doing the work and acknowledging it, whenever a connection drops mid-delivery, and whenever a retry is triggered by something that had in fact succeeded.

The three-step shape is deliberate and is what makes this safe under failure. A naive store offers "have I seen this?" followed by "record that I have", and loses either way: mark before the handler runs and a crash mid-handler means the work never happens and never can, because the message now looks handled; mark after and two concurrent deliveries both pass the check. Claiming, then confirming only on success and releasing on failure, closes both.

Implementations must be safe under concurrent use and must make claim(java.lang.String) atomic: exactly one caller can hold a claim on an identifier at a time.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Interface
    Description
    static enum 
  • Method Summary

    Modifier and Type
    Method
    Description
    boolean
    claim(String messageId)
    Takes ownership of a message identifier, if nobody else has it.
    void
    confirm(String messageId)
    Records that the work for a claimed identifier completed.
    boolean
    isConfirmed(String messageId)
     
    void
    release(String messageId)
    Gives up a claim without recording completion, so the message can be tried again.
    tryClaim(String messageId)
    Takes ownership of a message identifier, and says why not when it cannot.
  • Method Details

    • claim

      boolean claim(String messageId)
      Takes ownership of a message identifier, if nobody else has it.
      Parameters:
      messageId - the identifier, normally Envelope.id()
      Returns:
      true when the caller now owns this identifier and should do the work; false when it is already confirmed or claimed elsewhere. Which of the two it is matters — only a confirmed one is a duplicate — so consumers call tryClaim(java.lang.String)
    • confirm

      void confirm(String messageId)
      Records that the work for a claimed identifier completed.

      After this, claim(java.lang.String) returns false for the identifier until the store forgets it, which is what stops a redelivery from repeating the work.

      Parameters:
      messageId - the identifier being confirmed
    • release

      void release(String messageId)
      Gives up a claim without recording completion, so the message can be tried again.

      Called when a handler fails. Without it a failed attempt would poison the identifier and the retry would be discarded as a duplicate, which turns a transient failure into permanent message loss.

      Parameters:
      messageId - the identifier being released
    • isConfirmed

      boolean isConfirmed(String messageId)
      Parameters:
      messageId - the identifier to test
      Returns:
      whether the work for this identifier is already known to have completed
    • tryClaim

      default IdempotencyStore.ClaimResult tryClaim(String messageId)
      Takes ownership of a message identifier, and says why not when it cannot.

      claim(java.lang.String) answers false for two different situations, and a consumer must not treat them alike. A confirmed identifier is finished work, and a redelivery of it is a duplicate to acknowledge. A claimed but unconfirmed identifier is work nobody has finished: acknowledging that redelivery loses the message whenever the first handler died and its release failed too. So the consumer acknowledges only IdempotencyStore.ClaimResult.ALREADY_CONFIRMED, and sends IdempotencyStore.ClaimResult.IN_PROGRESS back to the queue to be tried again.

      The default asks claim(java.lang.String) and then isConfirmed(java.lang.String), which suits every store written against the two-state contract. The two calls are not one atomic step, and both races resolve safely: a confirmation landing in between reads as a duplicate, which it is, and a confirmation expiring in between reads as in progress, which only costs a retry.

      Parameters:
      messageId - the identifier, normally Envelope.id()
      Returns:
      whether the caller now owns the identifier, or why it does not