Skip to content

Commit 5f90928

Browse files
committed
[#2187] Add lock-free ConcurrentMessageAudit
Fully lock-free message audit using ConcurrentHashMap for producer lookup using computeIfAbsent and AtomicBitArrayBin for CAS-based per-producer bit operations. No synchronized blocks for audit hot path.
1 parent 4538cb4 commit 5f90928

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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 java.util.Iterator;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
23+
import org.apache.activemq.command.MessageId;
24+
import org.apache.activemq.command.ProducerId;
25+
import org.apache.activemq.util.AtomicBitArrayBin;
26+
import org.apache.activemq.util.IdGenerator;
27+
28+
/**
29+
* A lock-free message audit backed by {@link ConcurrentHashMap} and
30+
* {@link AtomicBitArrayBin}.
31+
*
32+
* <p>The upstream {@code ActiveMQMessageAudit} wraps every operation in a
33+
* single {@code synchronized(this)} block, serializing all threads regardless
34+
* of which producer they are working with. This class eliminates all
35+
* synchronization on the hot path:
36+
* <ul>
37+
* <li>{@link ConcurrentHashMap#computeIfAbsent} for lock-free producer
38+
* lookup and atomic insertion</li>
39+
* <li>{@link AtomicBitArrayBin} for CAS-based bit-level mutations &mdash;
40+
* no {@code synchronized} blocks anywhere in the hot path</li>
41+
* </ul>
42+
*
43+
* <p>Producer count is bounded by {@link #getMaximumNumberOfProducersToTrack()}.
44+
* When the limit is exceeded, entries are evicted in iteration order (approximate
45+
* FIFO). The per-producer audit window is controlled by {@link #getAuditDepth()}.
46+
*/
47+
public class ConcurrentMessageAudit {
48+
49+
public static final int DEFAULT_WINDOW_SIZE = 2048;
50+
public static final int MAXIMUM_PRODUCER_COUNT = 64;
51+
52+
private volatile int auditDepth;
53+
private volatile int maximumNumberOfProducersToTrack;
54+
private final ConcurrentHashMap<String, AtomicBitArrayBin> map;
55+
56+
public ConcurrentMessageAudit() {
57+
this(DEFAULT_WINDOW_SIZE, MAXIMUM_PRODUCER_COUNT);
58+
}
59+
60+
public ConcurrentMessageAudit(int auditDepth, int maximumNumberOfProducersToTrack) {
61+
this.auditDepth = auditDepth;
62+
this.maximumNumberOfProducersToTrack = maximumNumberOfProducersToTrack;
63+
this.map = new ConcurrentHashMap<>(maximumNumberOfProducersToTrack);
64+
}
65+
66+
public int getAuditDepth() {
67+
return auditDepth;
68+
}
69+
70+
public void setAuditDepth(int auditDepth) {
71+
this.auditDepth = auditDepth;
72+
}
73+
74+
public int getMaximumNumberOfProducersToTrack() {
75+
return maximumNumberOfProducersToTrack;
76+
}
77+
78+
public void setMaximumNumberOfProducersToTrack(int maximumNumberOfProducersToTrack) {
79+
this.maximumNumberOfProducersToTrack = maximumNumberOfProducersToTrack;
80+
evictExcess();
81+
}
82+
83+
public boolean isDuplicate(String id) {
84+
var seed = IdGenerator.getSeedFromId(id);
85+
if (seed == null) {
86+
return false;
87+
}
88+
var bab = getOrCreate(seed);
89+
var index = IdGenerator.getSequenceFromId(id);
90+
if (index >= 0) {
91+
return bab.setBit(index, true);
92+
}
93+
return false;
94+
}
95+
96+
public boolean isDuplicate(final MessageId id) {
97+
if (id == null) {
98+
return false;
99+
}
100+
var pid = id.getProducerId();
101+
if (pid == null) {
102+
return false;
103+
}
104+
var bab = getOrCreate(pid.toString());
105+
return bab.setBit(id.getProducerSequenceId(), true);
106+
}
107+
108+
public void rollback(final MessageId id) {
109+
if (id == null) {
110+
return;
111+
}
112+
var pid = id.getProducerId();
113+
if (pid == null) {
114+
return;
115+
}
116+
var bab = map.get(pid.toString());
117+
if (bab != null) {
118+
bab.setBit(id.getProducerSequenceId(), false);
119+
}
120+
}
121+
122+
public void rollback(final String id) {
123+
var seed = IdGenerator.getSeedFromId(id);
124+
if (seed == null) {
125+
return;
126+
}
127+
var bab = map.get(seed);
128+
if (bab != null) {
129+
long index = IdGenerator.getSequenceFromId(id);
130+
bab.setBit(index, false);
131+
}
132+
}
133+
134+
public boolean isInOrder(final String id) {
135+
if (id == null) {
136+
return true;
137+
}
138+
var seed = IdGenerator.getSeedFromId(id);
139+
if (seed == null) {
140+
return true;
141+
}
142+
var bab = map.get(seed);
143+
if (bab != null) {
144+
var index = IdGenerator.getSequenceFromId(id);
145+
return bab.isInOrder(index);
146+
}
147+
return true;
148+
}
149+
150+
public boolean isInOrder(final MessageId id) {
151+
if (id == null) {
152+
return false;
153+
}
154+
var pid = id.getProducerId();
155+
if (pid == null) {
156+
return false;
157+
}
158+
var bab = getOrCreate(pid.toString());
159+
return bab.isInOrder(id.getProducerSequenceId());
160+
}
161+
162+
public long getLastSeqId(ProducerId id) {
163+
var bab = map.get(id.toString());
164+
if (bab != null) {
165+
return bab.getLastSetIndex();
166+
}
167+
return -1;
168+
}
169+
170+
public void clear() {
171+
map.clear();
172+
}
173+
174+
public int getProducerCount() {
175+
return map.size();
176+
}
177+
178+
private AtomicBitArrayBin getOrCreate(String key) {
179+
var bab = map.get(key);
180+
if (bab != null) {
181+
return bab;
182+
}
183+
bab = map.computeIfAbsent(key, k -> new AtomicBitArrayBin(auditDepth));
184+
evictExcess();
185+
return bab;
186+
}
187+
188+
private void evictExcess() {
189+
var max = maximumNumberOfProducersToTrack;
190+
while (map.size() > max) {
191+
var it = map.keySet().iterator();
192+
if (it.hasNext()) {
193+
it.next();
194+
it.remove();
195+
} else {
196+
break;
197+
}
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)