Skip to content

Commit 61fcd5a

Browse files
committed
[#2187] Add CaffeineMessageAudit
Message audit using Caffeine cache with size-based TinyLfu eviction paired with lock-free AtomicBitArrayBin for per-producer bit operations. Provides bounded producer tracking with automatic eviction, compared to ConcurrentMessageAudit ConcurrentHashMap.
1 parent 5f90928 commit 61fcd5a

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

activemq-client/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
<scope>provided</scope>
6767
</dependency>
6868

69+
<dependency>
70+
<groupId>com.github.ben-manes.caffeine</groupId>
71+
<artifactId>caffeine</artifactId>
72+
<version>3.2.4</version>
73+
</dependency>
74+
6975
<!-- =============================== -->
7076
<!-- Testing Dependencies -->
7177
<!-- =============================== -->
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.activemq;
18+
19+
import com.github.benmanes.caffeine.cache.Cache;
20+
import com.github.benmanes.caffeine.cache.Caffeine;
21+
22+
import org.apache.activemq.command.MessageId;
23+
import org.apache.activemq.command.ProducerId;
24+
import org.apache.activemq.util.AtomicBitArrayBin;
25+
import org.apache.activemq.util.IdGenerator;
26+
27+
/**
28+
* A message audit backed by Caffeine cache and {@link AtomicBitArrayBin}.
29+
*
30+
* <p>Uses Caffeine's {@link Cache} with size-based eviction (TinyLfu policy)
31+
* for bounded producer tracking, paired with lock-free
32+
* {@link AtomicBitArrayBin} for per-producer bit operations.
33+
*
34+
* <p>Producer count is bounded by {@link #getMaximumNumberOfProducersToTrack()}.
35+
* Caffeine's TinyLfu admission policy evicts entries based on frequency and
36+
* recency, providing near-optimal hit rates. The per-producer audit window is
37+
* controlled by {@link #getAuditDepth()}.
38+
*
39+
* <p>All per-producer bit operations are lock-free via {@link AtomicBitArrayBin}.
40+
* The only synchronization points are within Caffeine's internal structures
41+
* for cache management.
42+
*/
43+
public class CaffeineMessageAudit {
44+
45+
public static final int DEFAULT_WINDOW_SIZE = 2048;
46+
public static final int MAXIMUM_PRODUCER_COUNT = 64;
47+
48+
private volatile int auditDepth;
49+
private volatile int maximumNumberOfProducersToTrack;
50+
private volatile Cache<String, AtomicBitArrayBin> cache;
51+
52+
public CaffeineMessageAudit() {
53+
this(DEFAULT_WINDOW_SIZE, MAXIMUM_PRODUCER_COUNT);
54+
}
55+
56+
public CaffeineMessageAudit(int auditDepth, int maximumNumberOfProducersToTrack) {
57+
this.auditDepth = auditDepth;
58+
this.maximumNumberOfProducersToTrack = maximumNumberOfProducersToTrack;
59+
this.cache = buildCache(maximumNumberOfProducersToTrack);
60+
}
61+
62+
public int getAuditDepth() {
63+
return auditDepth;
64+
}
65+
66+
public void setAuditDepth(int auditDepth) {
67+
this.auditDepth = auditDepth;
68+
}
69+
70+
public int getMaximumNumberOfProducersToTrack() {
71+
return maximumNumberOfProducersToTrack;
72+
}
73+
74+
public void setMaximumNumberOfProducersToTrack(int maximumNumberOfProducersToTrack) {
75+
this.maximumNumberOfProducersToTrack = maximumNumberOfProducersToTrack;
76+
var newCache = buildCache(maximumNumberOfProducersToTrack);
77+
newCache.putAll(this.cache.asMap());
78+
this.cache = newCache;
79+
}
80+
81+
public boolean isDuplicate(String id) {
82+
var seed = IdGenerator.getSeedFromId(id);
83+
if (seed == null) {
84+
return false;
85+
}
86+
var bab = cache.get(seed, k -> new AtomicBitArrayBin(auditDepth));
87+
var index = IdGenerator.getSequenceFromId(id);
88+
if (index >= 0) {
89+
return bab.setBit(index, true);
90+
}
91+
return false;
92+
}
93+
94+
public boolean isDuplicate(final MessageId id) {
95+
if (id == null) {
96+
return false;
97+
}
98+
var pid = id.getProducerId();
99+
if (pid == null) {
100+
return false;
101+
}
102+
var bab = cache.get(pid.toString(), k -> new AtomicBitArrayBin(auditDepth));
103+
return bab.setBit(id.getProducerSequenceId(), true);
104+
}
105+
106+
public void rollback(final MessageId id) {
107+
if (id == null) {
108+
return;
109+
}
110+
var pid = id.getProducerId();
111+
if (pid == null) {
112+
return;
113+
}
114+
var bab = cache.getIfPresent(pid.toString());
115+
if (bab != null) {
116+
bab.setBit(id.getProducerSequenceId(), false);
117+
}
118+
}
119+
120+
public void rollback(final String id) {
121+
var seed = IdGenerator.getSeedFromId(id);
122+
if (seed == null) {
123+
return;
124+
}
125+
var bab = cache.getIfPresent(seed);
126+
if (bab != null) {
127+
var index = IdGenerator.getSequenceFromId(id);
128+
bab.setBit(index, false);
129+
}
130+
}
131+
132+
public boolean isInOrder(final String id) {
133+
if (id == null) {
134+
return true;
135+
}
136+
var seed = IdGenerator.getSeedFromId(id);
137+
if (seed == null) {
138+
return true;
139+
}
140+
var bab = cache.getIfPresent(seed);
141+
if (bab != null) {
142+
var index = IdGenerator.getSequenceFromId(id);
143+
return bab.isInOrder(index);
144+
}
145+
return true;
146+
}
147+
148+
public boolean isInOrder(final MessageId id) {
149+
if (id == null) {
150+
return false;
151+
}
152+
var pid = id.getProducerId();
153+
if (pid == null) {
154+
return false;
155+
}
156+
var bab = cache.get(pid.toString(), k -> new AtomicBitArrayBin(auditDepth));
157+
return bab.isInOrder(id.getProducerSequenceId());
158+
}
159+
160+
public long getLastSeqId(ProducerId id) {
161+
var bab = cache.getIfPresent(id.toString());
162+
if (bab != null) {
163+
return bab.getLastSetIndex();
164+
}
165+
return -1;
166+
}
167+
168+
public void clear() {
169+
cache.invalidateAll();
170+
}
171+
172+
public int getProducerCount() {
173+
cache.cleanUp();
174+
return (int) cache.estimatedSize();
175+
}
176+
177+
private static Cache<String, AtomicBitArrayBin> buildCache(int maxSize) {
178+
return Caffeine.newBuilder()
179+
.maximumSize(maxSize)
180+
.build();
181+
}
182+
}

0 commit comments

Comments
 (0)