@@ -47,7 +47,7 @@ public class CaffeineMessageAudit {
4747
4848 private volatile int auditDepth ;
4949 private volatile int maximumNumberOfProducersToTrack ;
50- private volatile Cache <String , AtomicBitArrayBin > cache ;
50+ private final Cache <String , AtomicBitArrayBin > cache ;
5151
5252 public CaffeineMessageAudit () {
5353 this (DEFAULT_WINDOW_SIZE , MAXIMUM_PRODUCER_COUNT );
@@ -73,22 +73,22 @@ public int getMaximumNumberOfProducersToTrack() {
7373
7474 public void setMaximumNumberOfProducersToTrack (int maximumNumberOfProducersToTrack ) {
7575 this .maximumNumberOfProducersToTrack = maximumNumberOfProducersToTrack ;
76- var newCache = buildCache (maximumNumberOfProducersToTrack );
77- newCache .putAll (this .cache .asMap ());
78- this .cache = newCache ;
76+ // Resize in place: rebuilding and swapping the cache would lose
77+ // entries inserted concurrently with the copy
78+ cache .policy ().eviction ()
79+ .ifPresent (eviction -> eviction .setMaximum (maximumNumberOfProducersToTrack ));
7980 }
8081
8182 public boolean isDuplicate (String id ) {
8283 var seed = IdGenerator .getSeedFromId (id );
8384 if (seed == null ) {
8485 return false ;
8586 }
86- var bab = cache .get (seed , k -> new AtomicBitArrayBin (auditDepth ));
8787 var index = IdGenerator .getSequenceFromId (id );
88- if (index >= 0 ) {
89- return bab . setBit ( index , true ) ;
88+ if (index < 0 ) {
89+ return false ;
9090 }
91- return false ;
91+ return markSeen ( seed , index ) ;
9292 }
9393
9494 public boolean isDuplicate (final MessageId id ) {
@@ -99,8 +99,21 @@ public boolean isDuplicate(final MessageId id) {
9999 if (pid == null ) {
100100 return false ;
101101 }
102- var bab = cache .get (pid .toString (), k -> new AtomicBitArrayBin (auditDepth ));
103- return bab .setBit (id .getProducerSequenceId (), true );
102+ return markSeen (pid .toString (), id .getProducerSequenceId ());
103+ }
104+
105+ /**
106+ * Record the index against the bin for the key. If the bin is evicted
107+ * between lookup and the bit write, the write lands in an orphaned bin
108+ * and the next occurrence of the id is not detected as a duplicate.
109+ * This residual race is accepted: eviction only fires under producer
110+ * oversubscription, membership re-check-and-retry in that regime was
111+ * measured at a 2-5x throughput cost while the recreated entry is
112+ * immediately evicted again, and the failure direction (duplicate
113+ * redelivery) is tolerable under at-least-once delivery.
114+ */
115+ private boolean markSeen (String key , long index ) {
116+ return cache .get (key , k -> new AtomicBitArrayBin (auditDepth )).setBit (index , true );
104117 }
105118
106119 public void rollback (final MessageId id ) {
@@ -111,22 +124,26 @@ public void rollback(final MessageId id) {
111124 if (pid == null ) {
112125 return ;
113126 }
114- var bab = cache .getIfPresent (pid .toString ());
115- if (bab != null ) {
116- bab .setBit (id .getProducerSequenceId (), false );
117- }
127+ var seqId = id .getProducerSequenceId ();
128+ cache .asMap ().computeIfPresent (pid .toString (), (k , bab ) -> {
129+ bab .setBit (seqId , false );
130+ return bab ;
131+ });
118132 }
119133
120134 public void rollback (final String id ) {
121135 var seed = IdGenerator .getSeedFromId (id );
122136 if (seed == null ) {
123137 return ;
124138 }
125- var bab = cache .getIfPresent (seed );
126- if (bab != null ) {
127- var index = IdGenerator .getSequenceFromId (id );
128- bab .setBit (index , false );
139+ var index = IdGenerator .getSequenceFromId (id );
140+ if (index < 0 ) {
141+ return ;
129142 }
143+ cache .asMap ().computeIfPresent (seed , (k , bab ) -> {
144+ bab .setBit (index , false );
145+ return bab ;
146+ });
130147 }
131148
132149 public boolean isInOrder (final String id ) {
0 commit comments