MetricManager timers and histograms sample into ExponentiallyDecayingReservoir, whose read lock contends under concurrent store operations
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Refactor
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- java
- Domain
- backend, observability, performance
Research direction
Start in janusgraph-core/src/main/java/org/janusgraph/util/stats/MetricManager.java at getTimer and getHistogram, then inspect MetricInstrumentedStore.runWithMetrics to understand the affected operations. Confirm how MetricRegistry creates these metrics and preserve their names, reporters, and JMX output while using the lock-free reservoir. The change is done when all timers and histograms obtained through MetricManager avoid the contended default reservoir.
Written by the indexing model from the issue text.
Description
- Version:
master(b54c363) - Storage Backend: any (the reservoir is in
janusgraph-core) - Mixed Index Backend: any
- Expected Behavior: with
metrics.enabled=true, recording a duration intostores.getSlice.time,stores.mutate.timeorstores.acquireLock.timeshould cost a few atomic increments, so that instrumentation stays a small fraction of the storage call it measures. - Current Behavior: every
Timer.updatetakes the read side of aReentrantReadWriteLockinsideExponentiallyDecayingReservoir. Under many concurrent transactions the compare-and-set on that lock's state word becomes the hottest code in the process.
Details
MetricManager.getTimer and getHistogram call MetricRegistry.timer(name) / histogram(name):
Those overloads build the metric with Dropwizard's default reservoir, ExponentiallyDecayingReservoir. Its update method is rescaleIfNeeded(); lockForRegularUsage(); values.put(...); unlockForRegularUsage(); where the regular-usage lock is a ReentrantReadWriteLock.readLock(). Readers do not block each other, but every acquire and every release is a CAS on the lock's single shared state word, plus a ThreadLocal lookup for the per-thread hold count. When many threads update the same timer at once the CASes collide and spin.
MetricInstrumentedStore.runWithMetrics wraps every getSlice, mutate and acquireLock in exactly such a timer, and with the default metrics.merge-stores=true all stores share the same three names, so every backend call in the process funnels into three reservoirs.
A CPU profile of a write-heavy workload on a 128-thread Gremlin Server, taken with async-profiler over 12 minutes, showed:
| Frame | Flat % |
|---|---|
VarHandleInts$FieldInstanceReadWrite.compareAndSet (from AbstractQueuedSynchronizer.compareAndSetState) |
17.2 |
ReentrantReadWriteLock$Sync.tryAcquireShared |
9.2 |
ReentrantReadWriteLock$Sync.tryReleaseShared |
9.0 |
ReentrantReadWriteLock$Sync.fullTryAcquireShared |
5.7 |
AbstractQueuedSynchronizer.getState |
3.8 |
ReentrantReadWriteLock$ReadLock.lock / unlock (self) |
4.6 |
ThreadLocal get/set/remove for the read-hold counter |
~4 |
All of it under com.codahale.metrics.Timer.update → Histogram.update → ExponentiallyDecayingReservoir.update → lockForRegularUsage / unlockForRegularUsage. The storage backend's own calls were under 1% of samples in the same profile. That workload was lock-call heavy, so the share is higher than a typical deployment would see, but the mechanism is the same for any deployment with metrics.enabled=true and enough concurrency.
Suggested Fix
Dropwizard Metrics 4.2 (JanusGraph is on 4.2.37) ships LockFreeExponentiallyDecayingReservoir: the same forward-decay sampling with the same size and alpha defaults, but the reservoir state lives in an immutable object swapped with a single CAS, and update does a putIfAbsent on a ConcurrentSkipListMap with no lock. Its documented trade-off is that a few updates racing with the hourly rescale may be lost, which does not matter for latency percentiles.
MetricRegistry has timer(String, MetricSupplier<Timer>) and histogram(String, MetricSupplier<Histogram>) overloads, so MetricManager can switch every timer and histogram it hands out without changing metric names, reporters or JMX output:
private static final MetricRegistry.MetricSupplier<Timer> TIMER_SUPPLIER =
() -> new Timer(LockFreeExponentiallyDecayingReservoir.builder().build());
public Timer getTimer(String name) {
return getRegistry().timer(name, TIMER_SUPPLIER);
}
Every timer and histogram in janusgraph-core, janusgraph-es and the backends goes through MetricManager, so this one change covers them all. Happy to open a PR.
- Dominant language
- Java
- Stars
- 5.8k
- Forks
- 1.2k
- Avg merge
- 19h 27m
- Merged PRs (30d)
- 21
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from JanusGraph/janusgraph
-
Difficulty 1/5 Under an hour Newbie friendliness 95/100
JanusGraph/janusgraph#4943 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 62/100
JanusGraph/janusgraph#1578 ·
Maintainers usually reply within 1 day
-
Transaction recovery takes a transaction whose final status it reads one poll later for a failed oneOpen
Difficulty 4/5 3-5 days Newbie friendliness 45/100
JanusGraph/janusgraph#4986 ·
Maintainers usually reply within 1 day
-
Closing a log can interrupt its send thread mid-write, which invalidates a BerkeleyJE environmentOpen
Difficulty 4/5 3-5 days Newbie friendliness 50/100
JanusGraph/janusgraph#4984 ·
Maintainers usually reply within 1 day
-
Definition edges an ordinary transaction commits don't reach the schema caches of other instancesOpen
Difficulty 5/5 Over a week Newbie friendliness 42/100
JanusGraph/janusgraph#4982 ·
Maintainers usually reply within 1 day
All issues in JanusGraph/janusgraph
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
openhab/openhab-core#5847 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
apache/parquet-java#3820 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
beehive-lab/jllm#187 ·