Class CircularConcurrentLinkedDeque<E>

java.lang.Object
java.util.AbstractCollection<E>
java.util.concurrent.ConcurrentLinkedDeque<E>
org.mockserver.collections.CircularConcurrentLinkedDeque<E>
All Implemented Interfaces:
Serializable, Iterable<E>, Collection<E>, Deque<E>, Queue<E>

public class CircularConcurrentLinkedDeque<E> extends ConcurrentLinkedDeque<E>
A bounded ConcurrentLinkedDeque that evicts the oldest element(s) once it reaches maxSize, invoking an optional callback on each evicted element.

Why the explicit size counter: ConcurrentLinkedDeque.size() is documented as an O(n) operation (it walks the whole list). The eviction check runs on every add(E)/offer(E), so relying on super.size() made each insertion O(n) once the deque was full — the hot path for MockServer's request/event log. Under a sustained request load this manifested as CPU usage that climbed as the log filled and stayed high (GitHub issue #2329). An AtomicInteger maintained by every mutating method makes size() and the eviction check O(1).

The counter is kept consistent by every size-changing method on this class (add(E), offer(E), addAll(java.util.Collection<? extends E>) (via add), remove(java.lang.Object), removeItem(E), clear(), and the internal eviction). Callers must mutate the deque only through these methods (MockServer's MockServerEventLog does); direct use of other inherited bulk mutators is not supported by this subclass.

Optional byte budget: in addition to the element-count bound, an optional maxBytes budget can be supplied together with a weigher (via the 4-arg constructor). Each element's weight is measured by the weigher on insertion and accumulated into totalBytes; whenever an insertion would push the running total over maxBytes the oldest elements are evicted first until it fits (or the deque is empty). This caps the heap held by the event log when individual entries are large (e.g. big LLM-capture bodies) rather than only by entry count. A single element whose weight alone exceeds maxBytes is still retained — the byte-eviction loop stops once the deque is empty so we never reject the incoming element. The budget is disabled when maxBytes <= 0 or the weigher is null, in which case the deque behaves exactly as the count-bounded version.

Eviction accounting: getEvictedCount() reports how many elements this deque has silently discarded to stay within its bounds. This deque is the only layer that can distinguish an eviction (evidence lost because the buffer was full) from an intentional clear() or a targeted removeItem(Object) — both of which invoke the same eviction callback but are deliberate, not lossy. Only pollAndEvict() increments the counter; clear() resets it, because after an explicit clear the caller has declared the previous contents irrelevant. MockServer's event log surfaces this count so a verification can tell "this never happened" apart from "the evidence was discarded".

Author:
jamesdbloom
See Also:
  • Constructor Details

    • CircularConcurrentLinkedDeque

      public CircularConcurrentLinkedDeque(int maxSize, Consumer<E> onEvictCallback)
    • CircularConcurrentLinkedDeque

      public CircularConcurrentLinkedDeque(int maxSize, long maxBytes, ToLongFunction<E> weigher, Consumer<E> onEvictCallback)
  • Method Details

    • setMaxSize

      public void setMaxSize(int maxSize)
      Resize the element-count bound. A SHRINK takes effect immediately: the oldest elements are evicted (firing the eviction callback, exactly as an overflow eviction would) until the deque fits the new bound, rather than waiting for the next add(E). This is what makes a live maxLogEntries change via PUT /mockserver/configuration release memory at once instead of only after further traffic.
    • setMaxBytes

      public void setMaxBytes(long maxBytes)
      Resize the optional byte budget. Like setMaxSize(int) a shrink takes effect immediately, evicting the oldest elements until the running byte total fits the new budget. No-op accounting when the budget is disabled (maxBytes <= 0 or no weigher).
    • size

      public int size()
      O(1) size, backed by an internal counter (unlike ConcurrentLinkedDeque.size() which is O(n)).
      Specified by:
      size in interface Collection<E>
      Specified by:
      size in interface Deque<E>
      Overrides:
      size in class ConcurrentLinkedDeque<E>
    • isEmpty

      public boolean isEmpty()
      Specified by:
      isEmpty in interface Collection<E>
      Overrides:
      isEmpty in class ConcurrentLinkedDeque<E>
    • add

      public boolean add(E element)
      Specified by:
      add in interface Collection<E>
      Specified by:
      add in interface Deque<E>
      Specified by:
      add in interface Queue<E>
      Overrides:
      add in class ConcurrentLinkedDeque<E>
    • addAll

      public boolean addAll(Collection<? extends E> collection)
      Specified by:
      addAll in interface Collection<E>
      Specified by:
      addAll in interface Deque<E>
      Overrides:
      addAll in class ConcurrentLinkedDeque<E>
    • offer

      public boolean offer(E element)
      Specified by:
      offer in interface Deque<E>
      Specified by:
      offer in interface Queue<E>
      Overrides:
      offer in class ConcurrentLinkedDeque<E>
    • getEvictedCount

      public long getEvictedCount()
      Number of elements silently discarded to stay within maxSize/maxBytes since construction or the last clear(). A non-zero value means this deque no longer holds a complete record of what was added to it — any consumer that reasons about absence (e.g. a "this never happened" verification) must treat its answer as unreliable.
    • resetEvictedCount

      public void resetEvictedCount()
      Forget past evictions without touching the contents. Used when a caller declares everything recorded so far irrelevant but does not (or cannot) physically empty the deque — e.g. MockServer's "clear everything" path, which tombstones entries rather than removing them. Without this, one rollover would taint every later query for the lifetime of the process.
    • clear

      public void clear()
      Specified by:
      clear in interface Collection<E>
      Overrides:
      clear in class ConcurrentLinkedDeque<E>
    • remove

      @Deprecated public boolean remove(Object o)
      Deprecated.
      use removeItem instead
      Specified by:
      remove in interface Collection<E>
      Specified by:
      remove in interface Deque<E>
      Overrides:
      remove in class ConcurrentLinkedDeque<E>
    • removeItem

      public boolean removeItem(E e)