mwashburn2 opened a new pull request, #2671:
URL: https://github.com/apache/activemq/pull/2671

   ## Problem
   
   `SystemUsage.getMemoryUsage()` is one `MemoryUsage` instance shared by every 
destination (`BaseDestination` uses it directly by default). Every message's 
reference count transitions (`Message.incrementReferenceCount` / 
`decrementReferenceCount`) take its `ReentrantReadWriteLock` **write** lock via 
`increaseUsage` / `decreaseUsage`. Every `Topic.send` / `Queue.send` takes the 
**read** lock via `isFull()`, unconditionally and before the 
producer-flow-control check. Cursor `hasSpace()` also takes it via 
`getPercentUsage()`.
   
   On a busy many-core broker this produces a lock convoy. Throughput is capped 
by park/unpark hand-offs on that one lock, even though CPU and network have 
plenty of headroom.
   
   ### Production evidence (5.18.x, 64 cores, ~600 transport threads, mostly 
non-persistent topics)
   
   Five thread dumps were taken about 30s apart. The global `MemoryUsage` RRWL 
was the dominant contention point in every one:
   
   | Dump | Threads parked on global usage RRWL | Threads parked on queue 
cursor RRWLs (holders blocked on the global lock) |
   |---|---|---|
   | 1 | 49 | 15 |
   | 2 | 34 | 2 |
   | 3 | 26 | 45 |
   | 4 | 60 | 26 |
   | 5 | 58 | 8 |
   
   Breakdown from one dump, by operation on the lock:
   
   | Operation | Waiters |
   |---|---|
   | `isFull` ← `Topic.send` | 38 |
   | `isFull` ← `Queue.send` | 9 |
   | `decreaseUsage` ← `decrementReferenceCount` | 7 |
   | `increaseUsage` ← `incrementReferenceCount` | 3 |
   | `getPercentUsage` ← `isParentFull` | 3 |
   
   No thread was ever RUNNABLE inside `setPercentUsage` or `fireEvent`. The 
cost is purely lock hand-off, not work done under the lock.
   
   Waiting threads often hold a `Topic` monitor or a queue cursor lock. Other 
transport threads then block behind them and stop reading their sockets, and 
producers stall in `send()` while broker CPU sits mostly idle. The enqueue rate 
stayed flat at a steady ceiling regardless of offered load.
   
   The memory limit was never close to being hit, so nobody was actually 
waiting for space.
   
   ## Change
   
   - `MemoryUsage.usage` is an `AtomicLong`. `increaseUsage` / `decreaseUsage` 
/ `getUsage` no longer lock.
   - `Usage.percentUsage`, `limiter`, `percentUsageMinDelta`, `usagePortion` 
and `DefaultUsageCapacity.limit` are `volatile`. `getPercentUsage()`, 
`getLimit()`, `getUsagePortion()`, `getPercentUsageMinDelta()` and 
`MemoryUsage.isFull()` / `isFull(int)` are lock-free.
   - `percentUsage` is still **written only under the write lock**, and only 
when the computed value actually changes. `setPercentUsage` → `fireEvent` and 
the `waitForSpace` condition signalling therefore keep their existing 
semantics. Most inc/dec calls do not move the percentage, so the lock is rarely 
taken.
   - `waitForSpace()` / `waitForSpace(long)` return immediately when not full. 
They only lock and await when full, rechecking under the lock.
   - `percentUsage` is recomputed on `setUsage`, `onLimitChange` and 
`setPercentUsageMinDelta`, so it cannot go stale.
   - `StoreUsage`, `TempUsage`, `JobSchedulerUsage` and `PercentLimitUsage` are 
unchanged. They still use the locked base paths.
   
   ### Why it is safe
   
   - Updaters publish the new usage with `addAndGet` (volatile write), then 
compare the freshly computed percent to the volatile `percentUsage`.
   - Whoever changes `percentUsage` does so under the write lock and loops (`do 
{ v = calc(); setPercentUsage(v); } while (v != calc())`). The last updater to 
publish therefore always leaves `percentUsage` consistent with the current 
usage, and no stale value can survive.
   - Readers (`isFull`, `getPercentUsage`) may see a value one update behind. 
That was already true for callers using the result after releasing the read 
lock.
   - `LongAdder` was considered and rejected because `sum()` is not an exact 
snapshot.
   
   ## Microbenchmark
   
   The benchmark uses 16 destination `MemoryUsage`s sharing one parent (limit 
123 GB, about 48% used). Each op is `increaseUsage(1024); isFull(); 
getPercentUsage(); parent.isFull(); decreaseUsage(1024)`, which mirrors the 
per-message broker pattern. Results from JDK 25 on 64 cores:
   
   | Threads | Before (ops/s) | After (ops/s) | Speedup |
   |---|---|---|---|
   | 1 | 19.4M | 93.8M | 4.8x |
   | 8 | 6.3M | 34.5M | 5.5x |
   | 32 | 5.5M | 14.3M | 2.6x |
   | 64 | 5.8M | 24.3M | 4.2x |
   
   ## Tests
   
   - New `MemoryUsageLockFreeTest` (9 tests) covers:
     - concurrent inc/dec exactness
     - percent consistency after concurrent updates
     - listener events on threshold crossing
     - `waitForSpace` blocking and releasing, with and without timeout
     - parent propagation
     - limit and min-delta changes
     - `setUsage`
   - These existing tests pass:
   
   | Module | Tests |
   |---|---|
   | `activemq-client` | `MemoryUsageTest`, `MemoryUsageConcurrencyTest` |
   | `activemq-unit-tests` | `MemoryUsageBlockResumeTest`, 
`UsageBlockedDispatchTest`, `MemoryUsageCleanupTest`, `MemoryLimitTest`, 
`CompositeMessageCursorUsageTest`, `ProducerFlowControlTest`, 
`ProducerFlowControlSendFailTest`, `TopicProducerFlowControlTest`, 
`StoreUsageTest`, `StoreUsageLimitsTest`, 
`QueueMemoryAndStoreUsageCleanupTest`, `QueueOptimizedDispatchExceptionTest` |
   
   Opening as a draft for feedback on the approach before a wider test run.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact


Reply via email to