Skip to content

Commit 47261e7

Browse files
committed
[#2275] Lock-free Usage accounting via AtomicLong
Replace the exclusive-lock counter in MemoryUsage with an AtomicLong and cache the current percentUsage bucket as absolute value bounds (PercentBounds) in base Usage. Mutations and reads compare the usage value against the cached bounds and only take the usageLock when a bound is crossed, where setPercentUsage recalculates the percent and bounds and fires the usual listener events and waitForSpace signals. The locked path recomputes after publishing until stable so a concurrent update cannot leave the published percent stale. Storage usage values change externally via store.size(), so base isFull(int) and getPercentUsage() use the same bounds check to skip the write lock on reads; the StoreUsage and TempUsage per-read recompute overrides are removed and JobSchedulerUsage now refreshes its percent on read.
1 parent 6f45655 commit 47261e7

7 files changed

Lines changed: 502 additions & 65 deletions

File tree

activemq-broker/src/main/java/org/apache/activemq/usage/StoreUsage.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,6 @@ public void setStore(PersistenceAdapter store) {
6767
}
6868
}
6969

70-
@Override
71-
public int getPercentUsage() {
72-
usageLock.writeLock().lock();
73-
try {
74-
percentUsage = caclPercentUsage();
75-
return super.getPercentUsage();
76-
} finally {
77-
usageLock.writeLock().unlock();
78-
}
79-
}
80-
81-
8270
@Override
8371
protected void updateLimitBasedOnPercent() {
8472
usageLock.writeLock().lock();

activemq-broker/src/main/java/org/apache/activemq/usage/TempUsage.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,6 @@ public TempUsage(TempUsage parent, String name) {
4747
updateLimitBasedOnPercent();
4848
}
4949

50-
@Override
51-
public int getPercentUsage() {
52-
if (store != null) {
53-
usageLock.writeLock().lock();
54-
try {
55-
percentUsage = caclPercentUsage();
56-
} finally {
57-
usageLock.writeLock().unlock();
58-
}
59-
}
60-
return super.getPercentUsage();
61-
}
62-
6350
@Override
6451
protected long retrieveUsage() {
6552
if (store == null) {

activemq-client/src/main/java/org/apache/activemq/usage/MemoryUsage.java

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.activemq.usage;
1818

1919
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.atomic.AtomicLong;
2021

2122
/**
2223
* Used to keep track of how much of something is being used so that a
@@ -28,7 +29,15 @@
2829
*/
2930
public class MemoryUsage extends Usage<MemoryUsage> {
3031

31-
private long usage;
32+
// Lock-free usage accounting: the counter is an AtomicLong so increase/decrease never
33+
// take an exclusive lock; the usageLock is only taken when the counter crosses out of the
34+
// current percent bucket (at most ~100/percentUsageMinDelta times per limit traversal),
35+
// which preserves listener events and waitForSpace signalling. AtomicLong was chosen over
36+
// a striped LongAdder after benchmarking showed equal throughput at 1-22 producer threads
37+
// on an 11-core machine, while AtomicLong keeps get() exact, makes setUsage() a plain
38+
// atomic set, and avoids per-instance cell inflation.
39+
private final AtomicLong usage = new AtomicLong();
40+
3241

3342
public MemoryUsage() {
3443
this(null, null);
@@ -129,12 +138,8 @@ public boolean isFull() {
129138
if (parent != null && parent.isFull()) {
130139
return true;
131140
}
132-
usageLock.readLock().lock();
133-
try {
134-
return percentUsage >= 100;
135-
} finally {
136-
usageLock.readLock().unlock();
137-
}
141+
// percentUsage is volatile; no lock needed for a read.
142+
return percentUsage >= 100;
138143
}
139144

140145
/**
@@ -159,12 +164,15 @@ public void increaseUsage(long value) {
159164
return;
160165
}
161166

162-
usageLock.writeLock().lock();
163-
try {
164-
usage += value;
165-
setPercentUsage(caclPercentUsage());
166-
} finally {
167-
usageLock.writeLock().unlock();
167+
// INVARIANT: every usage.addAndGet() MUST be followed unconditionally by the bounds
168+
// check in the same method (no early return or throw between them). The liveness of
169+
// untimed waitForSpace() depends on it: the temporally last mutation compares the
170+
// complete counter value against the current bucket bounds, so a lasting
171+
// 100% -> <100% transition always reaches the locked updatePercent() path, which
172+
// signals waitForSpaceCondition. Breaking this ordering can strand waiters forever.
173+
final long v = usage.addAndGet(value);
174+
if (!bounds.contains(v)) {
175+
updatePercent();
168176
}
169177

170178
if (parent != null) {
@@ -182,31 +190,61 @@ public void decreaseUsage(long value) {
182190
return;
183191
}
184192

185-
usageLock.writeLock().lock();
186-
try {
187-
usage -= value;
188-
setPercentUsage(caclPercentUsage());
189-
} finally {
190-
usageLock.writeLock().unlock();
193+
// INVARIANT: addAndGet() must be followed unconditionally by the bounds check
194+
// (see increaseUsage for the full liveness rationale).
195+
final long v = usage.addAndGet(-value);
196+
if (!bounds.contains(v)) {
197+
updatePercent();
191198
}
192199

193200
if (parent != null) {
194201
parent.decreaseUsage(value);
195202
}
196203
}
197204

205+
/**
206+
* Cold path, entered only when the counter crosses out of the cached percent bucket.
207+
* Recomputes percentUsage from the live counter and publishes it via setPercentUsage()
208+
* (firing listener events and signalling waitForSpace waiters), which also installs the
209+
* new bucket bounds. The recompute-after-publish loop makes the update race-proof: after
210+
* publishing we re-read the live counter, and either we observe a concurrent mutation
211+
* (loop and correct), or that mutation's addAndGet follows our read in the counter's
212+
* synchronization order - in which case its bounds check is guaranteed to see the bounds
213+
* we just published and takes this path itself.
214+
*/
215+
private void updatePercent() {
216+
usageLock.writeLock().lock();
217+
try {
218+
int p;
219+
do {
220+
p = caclPercentUsage();
221+
setPercentUsage(p);
222+
} while (caclPercentUsage() != p);
223+
} finally {
224+
usageLock.writeLock().unlock();
225+
}
226+
}
227+
228+
229+
198230
@Override
199231
protected long retrieveUsage() {
200-
return usage;
232+
return usage.get();
201233
}
202234

203235
@Override
204236
public long getUsage() {
205-
return usage;
237+
return usage.get();
206238
}
207239

208-
public void setUsage(long usage) {
209-
this.usage = usage;
240+
/**
241+
* Sets the usage to the given value as a single atomic store; a concurrent
242+
* increase/decrease linearizes cleanly before or after it. Note: as with the historical
243+
* field assignment, this does not propagate an adjustment to the parent usage.
244+
*/
245+
public void setUsage(long value) {
246+
this.usage.set(value);
247+
updatePercent();
210248
}
211249

212250
public void setPercentOfJvmHeap(int percentOfJvmHeap) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.usage;
18+
19+
/**
20+
* Internal API. The absolute usage-value bounds {@code [lower, upper)} of the range that maps
21+
* to a Usage's current percentUsage bucket. Lock-free hot paths compare a usage value against
22+
* these two longs - no division or percent math - and only enter the locked
23+
* percent-recompute path when the value crosses out of the bucket.
24+
*
25+
* <p>Immutable, and held by {@link Usage} in a single volatile reference so the pair can never
26+
* tear (two independent volatile longs could be read as a wider interval and miss a real
27+
* crossing).
28+
*
29+
* <p>The bucket math matches {@code Usage.caclPercentUsage()} truncating-division semantics:
30+
* percent P (a multiple of percentUsageMinDelta d, against limit L) covers values in
31+
* {@code [ceil(P*L/100), ceil((P+d)*L/100))}. Bounds only need to be conservative - the locked
32+
* path always derives the percent from {@code caclPercentUsage()}, so an imprecise bound costs
33+
* at most an extra locked recompute, never a wrong percent.
34+
*/
35+
public final class PercentBounds {
36+
37+
/**
38+
* Sentinel whose range is empty, so any value registers as a crossing - forces the first
39+
* observation to take the locked initialization path.
40+
*/
41+
public static final PercentBounds ALWAYS_CROSS = new PercentBounds(0, 0);
42+
43+
public final long lower;
44+
public final long upper;
45+
46+
PercentBounds(long lower, long upper) {
47+
this.lower = lower;
48+
this.upper = upper;
49+
}
50+
51+
public boolean contains(long value) {
52+
return value >= lower && value < upper;
53+
}
54+
55+
/**
56+
* Bounds of the usage-value range that maps to the given percent bucket.
57+
* {@code limit == 0} pins the percent at 0 (matching caclPercentUsage), so the bucket is
58+
* unbounded. Negative percents (negative usage is an accounting-error state) collapse into
59+
* one bucket below zero so any recovery to {@code >= 0} re-enters the locked path.
60+
*/
61+
public static PercentBounds compute(int percent, long limit, int minDelta) {
62+
if (limit == 0) {
63+
return new PercentBounds(Long.MIN_VALUE, Long.MAX_VALUE);
64+
}
65+
if (percent < 0) {
66+
return new PercentBounds(Long.MIN_VALUE, 0);
67+
}
68+
final int delta = Math.max(1, minDelta);
69+
final long lower = percent == 0 ? 0 : ceilDivSaturated(percent, limit);
70+
final long upper = ceilDivSaturated((long) percent + delta, limit);
71+
return new PercentBounds(lower, upper);
72+
}
73+
74+
/** ceil(percent * limit / 100), saturating to Long.MAX_VALUE on overflow. */
75+
private static long ceilDivSaturated(long percent, long limit) {
76+
try {
77+
final long product = Math.multiplyExact(percent, limit);
78+
return product / 100 + (product % 100 == 0 ? 0 : 1);
79+
} catch (ArithmeticException overflow) {
80+
return Long.MAX_VALUE;
81+
}
82+
}
83+
84+
@Override
85+
public String toString() {
86+
return "PercentBounds[" + lower + "," + upper + ")";
87+
}
88+
}

activemq-client/src/main/java/org/apache/activemq/usage/Usage.java

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,26 @@ public abstract class Usage<T extends Usage> implements Service {
4242

4343
protected final ReentrantReadWriteLock usageLock = new ReentrantReadWriteLock();
4444
protected final Condition waitForSpaceCondition = usageLock.writeLock().newCondition();
45-
protected int percentUsage;
45+
// volatile so lock-free hot paths (isFull, percent-change detection) can read it without
46+
// taking the usageLock; all writes still happen under the writeLock via setPercentUsage().
47+
protected volatile int percentUsage;
48+
// The absolute usage-value bounds of the current percentUsage bucket, kept in one volatile
49+
// reference (immutable pair, cannot tear). Installed under the write lock whenever
50+
// percentUsage is published (setPercentUsage/refreshPercentUsage), which also covers limit
51+
// and percentUsageMinDelta changes via onLimitChange/setPercentUsageMinDelta. Lock-free
52+
// hot paths compare a usage value against it to decide if a locked recompute is needed.
53+
protected volatile PercentBounds bounds = PercentBounds.ALWAYS_CROSS;
4654
protected T parent;
4755
protected String name;
4856

4957
private UsageCapacity limiter = new DefaultUsageCapacity();
50-
private int percentUsageMinDelta = 1;
58+
private volatile int percentUsageMinDelta = 1;
5159
private final List<UsageListener> listeners = new CopyOnWriteArrayList<UsageListener>();
5260
private final boolean debug = LOG.isDebugEnabled();
5361
private float usagePortion = 1.0f;
5462
private final List<T> children = new CopyOnWriteArrayList<T>();
5563
private final List<Runnable> callbacks = new LinkedList<Runnable>();
56-
private int pollingTime = 100;
64+
private volatile int pollingTime = 100;
5765
private final AtomicBoolean started = new AtomicBoolean();
5866
private ThreadPoolExecutor executor;
5967

@@ -93,12 +101,12 @@ public boolean waitForSpace(long timeout, int highWaterMark) throws InterruptedE
93101
}
94102
usageLock.writeLock().lock();
95103
try {
96-
percentUsage = caclPercentUsage();
104+
refreshPercentUsage(caclPercentUsage());
97105
if (percentUsage >= highWaterMark) {
98106
long deadline = timeout > 0 ? System.currentTimeMillis() + timeout : Long.MAX_VALUE;
99107
long timeleft = deadline;
100108
while (timeleft > 0) {
101-
percentUsage = caclPercentUsage();
109+
refreshPercentUsage(caclPercentUsage());
102110
if (percentUsage >= highWaterMark) {
103111
waitForSpaceCondition.await(pollingTime, TimeUnit.MILLISECONDS);
104112
timeleft = deadline - System.currentTimeMillis();
@@ -121,9 +129,15 @@ public boolean isFull(int highWaterMark) {
121129
if (parent != null && parent.isFull(highWaterMark)) {
122130
return true;
123131
}
132+
// Fast path: while the usage value stays inside the cached percent bucket the
133+
// published percentUsage is still valid - no lock, no division. retrieveUsage() is
134+
// safe to call unlocked for every implementation (atomic counters or constant).
135+
if (bounds.contains(retrieveUsage())) {
136+
return percentUsage >= highWaterMark;
137+
}
124138
usageLock.writeLock().lock();
125139
try {
126-
percentUsage = caclPercentUsage();
140+
refreshPercentUsage(caclPercentUsage());
127141
return percentUsage >= highWaterMark;
128142
} finally {
129143
usageLock.writeLock().unlock();
@@ -216,21 +230,26 @@ public void setUsagePortion(float usagePortion) {
216230
}
217231

218232
public int getPercentUsage() {
219-
usageLock.readLock().lock();
220-
try {
221-
return percentUsage;
222-
} finally {
223-
usageLock.readLock().unlock();
233+
// Fresh-on-read without a lock: if the usage value has crossed out of the cached
234+
// percent bucket, take the write lock once and silently refresh (no listener events -
235+
// preserving the historical behavior of read-driven recomputes). Subclasses whose
236+
// usage value changes externally (StoreUsage/TempUsage/JobSchedulerUsage) get accurate
237+
// reads from this shared path instead of per-class write-locked overrides.
238+
if (!bounds.contains(retrieveUsage())) {
239+
usageLock.writeLock().lock();
240+
try {
241+
refreshPercentUsage(caclPercentUsage());
242+
} finally {
243+
usageLock.writeLock().unlock();
244+
}
224245
}
246+
return percentUsage;
225247
}
226248

227249
public int getPercentUsageMinDelta() {
228-
usageLock.readLock().lock();
229-
try {
230-
return percentUsageMinDelta;
231-
} finally {
232-
usageLock.readLock().unlock();
233-
}
250+
// volatile field - no lock needed; also avoids a nested read-lock acquisition when
251+
// called from subclass code already holding the write lock (MemoryUsage.computeBounds)
252+
return percentUsageMinDelta;
234253
}
235254

236255
/**
@@ -268,6 +287,7 @@ protected void setPercentUsage(int value) {
268287
try {
269288
int oldValue = percentUsage;
270289
percentUsage = value;
290+
bounds = PercentBounds.compute(value, limiter.getLimit(), percentUsageMinDelta);
271291
if (oldValue != value) {
272292
fireEvent(oldValue, value);
273293
}
@@ -276,6 +296,17 @@ protected void setPercentUsage(int value) {
276296
}
277297
}
278298

299+
/**
300+
* Silently refresh the cached percentUsage (no listener events, no waiter signalling) -
301+
* used by the internal recompute sites in waitForSpace(long,int) and isFull(int).
302+
* Must be called with the usageLock write lock held. Subclasses that cache values derived
303+
* from percentUsage override this to refresh them in the same critical section.
304+
*/
305+
protected void refreshPercentUsage(int value) {
306+
percentUsage = value;
307+
bounds = PercentBounds.compute(value, limiter.getLimit(), percentUsageMinDelta);
308+
}
309+
279310
protected int caclPercentUsage() {
280311
if (limiter.getLimit() == 0) {
281312
return 0;
@@ -432,6 +463,10 @@ public UsageCapacity getLimiter() {
432463
}
433464

434465
/**
466+
* Creation-time setter. Swapping the limiter on a live Usage does not trigger
467+
* onLimitChange(): percentUsage - and any subclass caches derived from it, such as
468+
* MemoryUsage's percent bucket bounds - remain stale until the next recompute.
469+
*
435470
* @param limiter
436471
* the limiter to set
437472
*/

0 commit comments

Comments
 (0)