Class InMemoryReplayCache

java.lang.Object
org.apache.juneau.commons.concurrent.InMemoryReplayCache
All Implemented Interfaces:
ReplayCache

public class InMemoryReplayCache extends Object implements ReplayCache
Built-in default ReplayCache: a per-process, ConcurrentHashMap-backed seen-identifier set with self-eviction bounded by each record's own expiresAtMs.

Not shareable across process instances by design. Two separate JVMs (or two instances behind a load balancer) each holding their own InMemoryReplayCache will not see each other's recorded identifiers, so an identifier consumed against one instance and then submitted again against the other is not caught. Consumers who need cross-node single-use enforcement must supply a ReplayCache backed by a store shared across every node instead of relying on this default.

Memory bound (size honestly). An identifier is retained only until its own expiresAtMs, so the map's steady-state size is bounded by the product of the distinct-identifier submission rate and the identifier lifetime — i.e. roughly the number of distinct, still-unexpired identifiers outstanding at once, plus at most one lifetime's worth of already-expired records awaiting the next sweep. Under a high rate with a long lifetime this is not inherently small; consumers should size accordingly, or supply a store-backed ReplayCache with server-side expiry. There is deliberately no hard cap that drops still-unexpired identifiers: doing so would silently weaken replay detection by letting an evicted-but-valid identifier be submitted again.

Eviction is throttled. Sweeping out already-expired records is an O(n) scan, so it runs at most once per DEFAULT_SWEEP_INTERVAL_MS rather than on every call (which would make checkAndRecord(java.lang.String, long) effectively O(n) per invocation under load). The sweep exists purely to bound memory.

Thread-safety. checkAndRecord(String, long) is safe for concurrent invocation: the check-and-record step is a single ConcurrentHashMap.putIfAbsent(K, V) call, so of any two concurrent calls with the same identifier, exactly one observes a first-seen (true) outcome. The throttled sweep is guarded by an atomic compare-and-set so at most one thread sweeps per interval.

Since:
10.0.0
  • Field Details

  • Constructor Details

  • Method Details

    • checkAndRecord

      public boolean checkAndRecord(String id, long expiresAtMs)
      Description copied from interface: ReplayCache
      Atomically checks whether the specified identifier has been observed before and records it as seen if not.

      Implementations own their own timeout. A call that blocks indefinitely (for example a remote store with no client-side deadline) blocks the caller with it. An implementation backed by a network store must bound its own call so a slow/unreachable backend surfaces promptly as a throw (which the caller resolves per its chosen ReplayCache.FailMode) or a return, never an unbounded stall.

      Specified by:
      checkAndRecord in interface ReplayCache
      Parameters:
      id - The identifier to check. Never null.
      expiresAtMs - The identifier's own absolute expiry, in epoch milliseconds. An implementation may use this to self-evict a record once it can no longer matter.
      Returns:
      true if this is the first time the identifier has been observed (the caller should proceed); false if it has been observed before (the caller should reject the submission as a replay).