Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.apache.activemq.usage;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
* Used to keep track of how much of something is being used so that a
Expand All @@ -28,7 +29,14 @@
*/
public class MemoryUsage extends Usage<MemoryUsage> {

private long usage;
// Lock-free usage accounting: the counter is an AtomicLong so increase/decrease never
// take an exclusive lock; the usageLock is only taken when the rounded percentUsage
// actually changes (at most ~100/percentUsageMinDelta times per limit traversal), which
// preserves listener events and waitForSpace signalling. AtomicLong was chosen over a
// striped LongAdder after benchmarking showed equal throughput at 1-22 producer threads
// on an 11-core machine, while AtomicLong keeps get() exact, makes setUsage() a plain
// atomic set, and avoids per-instance cell inflation.
private final AtomicLong usage = new AtomicLong();

public MemoryUsage() {
this(null, null);
Expand Down Expand Up @@ -129,12 +137,8 @@ public boolean isFull() {
if (parent != null && parent.isFull()) {
return true;
}
usageLock.readLock().lock();
try {
return percentUsage >= 100;
} finally {
usageLock.readLock().unlock();
}
// percentUsage is volatile; no lock needed for a read.
return percentUsage >= 100;
}

/**
Expand All @@ -159,13 +163,14 @@ public void increaseUsage(long value) {
return;
}

usageLock.writeLock().lock();
try {
usage += value;
setPercentUsage(caclPercentUsage());
} finally {
usageLock.writeLock().unlock();
}
// INVARIANT: every usage.add() MUST be followed unconditionally by
// maybeUpdatePercent() in the same method (no early return or throw between them).
// The liveness of untimed waitForSpace() depends on it: the temporally last mutation
// recomputes the percent from a sum that includes all completed updates, so a lasting
// 100% -> <100% transition always reaches the locked setPercentUsage() path, which
// signals waitForSpaceCondition. Breaking this ordering can strand waiters forever.
usage.addAndGet(value);
maybeUpdatePercent();

if (parent != null) {
parent.increaseUsage(value);
Expand All @@ -182,31 +187,52 @@ public void decreaseUsage(long value) {
return;
}

usageLock.writeLock().lock();
try {
usage -= value;
setPercentUsage(caclPercentUsage());
} finally {
usageLock.writeLock().unlock();
}
// INVARIANT: add() must be followed unconditionally by maybeUpdatePercent()
// (see increaseUsage for the full liveness rationale).
usage.addAndGet(-value);
maybeUpdatePercent();

if (parent != null) {
parent.decreaseUsage(value);
}
}

/**
* Fast-path percent maintenance: a dirty compare against the volatile percentUsage; only
* when the rounded percent has actually changed do we take the writeLock and run the
* existing setPercentUsage() (which fires listener events and signals waitForSpace
* waiters). setPercentUsage recomputes from the live sum under the lock, so the last
* writer always stores a fresh value and transient races self-correct on the next update.
*/
private void maybeUpdatePercent() {
if (caclPercentUsage() != percentUsage) {
usageLock.writeLock().lock();
try {
setPercentUsage(caclPercentUsage());
} finally {
usageLock.writeLock().unlock();
}
}
}

@Override
protected long retrieveUsage() {
return usage;
return usage.get();
}

@Override
public long getUsage() {
return usage;
return usage.get();
}

public void setUsage(long usage) {
this.usage = usage;
/**
* Sets the usage to the given value as a single atomic store; a concurrent
* increase/decrease linearizes cleanly before or after it. Note: as with the historical
* field assignment, this does not propagate an adjustment to the parent usage.
*/
public void setUsage(long value) {
this.usage.set(value);
maybeUpdatePercent();
}

public void setPercentOfJvmHeap(int percentOfJvmHeap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ public abstract class Usage<T extends Usage> implements Service {

protected final ReentrantReadWriteLock usageLock = new ReentrantReadWriteLock();
protected final Condition waitForSpaceCondition = usageLock.writeLock().newCondition();
protected int percentUsage;
// volatile so lock-free hot paths (isFull, percent-change detection) can read it without
// taking the usageLock; all writes still happen under the writeLock via setPercentUsage().
protected volatile int percentUsage;
protected T parent;
protected String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -37,6 +38,177 @@ public class MemoryUsageConcurrencyTest {

private static final Logger LOG = LoggerFactory.getLogger(MemoryUsageConcurrencyTest.class);

/**
* Liveness soak for the untimed waitForSpace(): unlike the timed variants (which poll),
* it blocks on waitForSpaceCondition and depends entirely on the 100% -> below-100%
* transition reaching the locked setPercentUsage() path that signals the condition.
* With lock-free accounting this rests on the invariant that every usage mutation is
* unconditionally followed by a percent re-check, so the temporally last mutation of a
* lasting drop always signals. Each round parks waiters at exactly 100%, races balanced
* churn pairs (usage never drops below full) against the bookkeeping, then issues a
* lasting one-byte drop WHILE churn is still running. After churn quiesces the usage is
* lastingly below the limit, so every waiter must be released.
*/
@Test
public void testUntimedWaitForSpaceLivenessSoak() throws Exception {
final int rounds = 150;
final int waiters = 4;
final int churners = 6;

for (int round = 0; round < rounds; round++) {
final MemoryUsage u = new MemoryUsage();
u.setLimit(100);
u.start();
final AtomicBoolean churnRunning = new AtomicBoolean(true);
final CountDownLatch released = new CountDownLatch(waiters);
final List<Thread> waiterThreads = new ArrayList<>();
final List<Thread> churnThreads = new ArrayList<>();
try {
u.increaseUsage(100); // exactly full

for (int w = 0; w < waiters; w++) {
final Thread t = new Thread(() -> {
try {
u.waitForSpace(); // untimed: no polling fallback
released.countDown();
} catch (InterruptedException ignored) {
}
});
t.setDaemon(true);
waiterThreads.add(t);
t.start();
}

// wait until every waiter is parked on the condition
final long deadline = System.currentTimeMillis() + 5000;
for (Thread t : waiterThreads) {
while (t.getState() != Thread.State.WAITING && System.currentTimeMillis() < deadline) {
Thread.yield();
}
assertEquals("round " + round + " waiter failed to park", Thread.State.WAITING, t.getState());
}

// balanced churn: increase-then-decrease pairs keep usage >= 100 while
// hammering the lock-free percent bookkeeping
for (int c = 0; c < churners; c++) {
final Thread t = new Thread(() -> {
while (churnRunning.get()) {
u.increaseUsage(3);
u.decreaseUsage(3);
}
});
t.setDaemon(true);
churnThreads.add(t);
t.start();
}
Thread.sleep(2);

// the lasting drop below 100%, deliberately concurrent with churn
u.decreaseUsage(1);

Thread.sleep(1);
churnRunning.set(false);
for (Thread t : churnThreads) {
t.join(5000);
}

// usage is now lastingly 99/100: every untimed waiter must have been signalled
assertTrue("round " + round + " untimed waitForSpace waiters not released: " + u,
released.await(10, TimeUnit.SECONDS));
} finally {
churnRunning.set(false);
u.stop();
}
}
}

@Test
public void testSetUsageSequential() {
final MemoryUsage u = new MemoryUsage();
u.setLimit(1000);
u.start();
try {
u.increaseUsage(100);
assertEquals(100, u.getUsage());
u.setUsage(500);
assertEquals(500, u.getUsage());
assertEquals(50, u.getPercentUsage());
u.setUsage(0);
assertEquals(0, u.getUsage());
assertEquals(0, u.getPercentUsage());
} finally {
u.stop();
}
}

/**
* setUsage() racing balanced increase/decrease pairs must leave the final usage within
* one in-flight operation per thread of the set value. Each worker performs complete
* increase(v);decrease(v) pairs, so after joining, the only legal deviations from the
* set target come from pairs that straddle the set's linearization point (or its
* non-atomic LongAdder.sum() sweep): at most one op of at most maxOp per thread, in
* either direction. A setUsage() built on LongAdder.reset() can additionally lose
* concurrent updates outright (reset() is documented as safe only with no concurrent
* updates), allowing drift beyond this bound.
*/
@Test
public void testConcurrentSetUsageDriftBounded() throws Exception {
final int threads = 16;
final int maxOp = 100;
final int rounds = 200;
final long target = 123456;

for (int round = 0; round < rounds; round++) {
final MemoryUsage u = new MemoryUsage();
u.setLimit(1L << 40);
u.start();
final AtomicBoolean running = new AtomicBoolean(true);
final CountDownLatch startLatch = new CountDownLatch(1);
final List<Thread> workers = new ArrayList<>();
try {
for (int t = 0; t < threads; t++) {
final int seed = round * 31 + t;
final Thread w = new Thread(() -> {
final Random r = new Random(seed);
try {
startLatch.await();
} catch (InterruptedException e) {
return;
}
while (running.get()) {
final int v = r.nextInt(maxOp) + 1;
u.increaseUsage(v);
u.decreaseUsage(v);
}
});
w.setDaemon(true);
workers.add(w);
w.start();
}

startLatch.countDown();
Thread.sleep(2);
u.setUsage(target);
Thread.sleep(2);
running.set(false);
for (Thread w : workers) {
w.join(5000);
}

final long drift = u.getUsage() - target;
final long bound = (long) threads * maxOp;
if (Math.abs(drift) > bound) {
LOG.info("Round {} drift {} exceeds bound {} : {}", round, drift, bound, u);
}
assertEquals("round " + round + " drift " + drift + " exceeds per-thread in-flight bound " + bound,
0, Math.abs(drift) > bound ? drift : 0);
} finally {
running.set(false);
u.stop();
}
}
}

@Test
public void testCycle() throws Exception {
final Random r = new Random(0xb4a14);
Expand Down
Loading