This is an automated email from the ASF dual-hosted git repository.

jojochuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 21cdfda6081 HDDS-9377. Replace MutableStat with lock-free 
ConcurrentMutableStat in OMLockMetrics and PerformanceMetrics (#11085)
21cdfda6081 is described below

commit 21cdfda6081c31893601b1985050f5d58259d243
Author: Andrey Yarovoy <[email protected]>
AuthorDate: Tue Aug 25 19:16:38 2026 +0300

    HDDS-9377. Replace MutableStat with lock-free ConcurrentMutableStat in 
OMLockMetrics and PerformanceMetrics (#11085)
    
    Generated-by: Claude Opus 4.8
---
 .../hadoop/ozone/util/ConcurrentMutableStat.java   | 134 ++++++++++
 .../hadoop/ozone/util/PerformanceMetrics.java      |   5 +-
 .../ozone/util/ConcurrentMutableStatBenchmark.java | 286 +++++++++++++++++++++
 .../ozone/util/TestConcurrentMutableStat.java      | 172 +++++++++++++
 .../apache/hadoop/ozone/om/lock/OMLockMetrics.java |  29 +--
 5 files changed, 607 insertions(+), 19 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/ConcurrentMutableStat.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/ConcurrentMutableStat.java
new file mode 100644
index 00000000000..fe7c512f473
--- /dev/null
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/ConcurrentMutableStat.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.util;
+
+import java.util.concurrent.atomic.LongAccumulator;
+import java.util.concurrent.atomic.LongAdder;
+import org.apache.hadoop.metrics2.MetricsRecordBuilder;
+import org.apache.hadoop.metrics2.lib.MutableStat;
+import org.apache.hadoop.metrics2.util.SampleStat;
+
+/**
+ * A {@link MutableStat} whose hot-path {@link #add(long)} is non-blocking
+ * under concurrent callers. Each call accumulates in {@link LongAdder} /
+ * {@link LongAccumulator} cells and the pending totals are drained into the
+ * parent's running state lazily — only when metrics are read via
+ * {@link #snapshot}, {@link #lastStat}, or {@link #toString}.
+ *
+ * <p>This avoids the {@code synchronized} contention of the base class when
+ * many threads release locks simultaneously and all attempt to record a
+ * measurement on the same metric instance.
+ *
+ * <p><b>Standard deviation accuracy:</b> {@code drainPending()} batches all
+ * pending samples except the min and max into a single
+ * {@code super.add(n, sum)} call. {@link 
org.apache.hadoop.metrics2.util.SampleStat}
+ * treats a batch as {@code n} identical samples equal to their mean, so the
+ * within-batch variance is lost. The reported standard deviation is therefore
+ * underestimated. Callers that need accurate standard deviation should use
+ * {@link org.apache.hadoop.metrics2.lib.MutableStat} directly, or pass
+ * {@code extended=false} to suppress the stdev metric.
+ */
+public class ConcurrentMutableStat extends MutableStat {
+
+  private final LongAdder pendingSum   = new LongAdder();
+  private final LongAdder pendingCount = new LongAdder();
+  /** Cell-striped min accumulator: identity = Long.MAX_VALUE, function = 
Math::min. */
+  private final LongAccumulator pendingMin = new LongAccumulator(Math::min, 
Long.MAX_VALUE);
+  /** Cell-striped max accumulator: identity = Long.MIN_VALUE, function = 
Math::max. */
+  private final LongAccumulator pendingMax = new LongAccumulator(Math::max, 
Long.MIN_VALUE);
+
+  public ConcurrentMutableStat(String name, String description,
+      String sampleName, String valueName, boolean extended) {
+    super(name, description, sampleName, valueName, extended);
+  }
+
+  /**
+   * Accumulates {@code value} without acquiring any lock.
+   * The value is reflected in consumers on the next {@link #snapshot} call.
+   * {@code setChanged()} is deferred to {@link #drainPending()} to avoid
+   * concurrent volatile writes on the same field from all calling threads.
+   */
+  @Override
+  public void add(long value) {
+    pendingSum.add(value);
+    pendingCount.increment();
+    pendingMin.accumulate(value);
+    pendingMax.accumulate(value);
+  }
+
+  @Override
+  public synchronized void snapshot(MetricsRecordBuilder builder, boolean all) 
{
+    drainPending();
+    super.snapshot(builder, all);
+  }
+
+  @Override
+  public SampleStat lastStat() {
+    drainPending();
+    return super.lastStat();
+  }
+
+  @Override
+  public String toString() {
+    drainPending();
+    return super.toString();
+  }
+
+  /**
+   * Moves accumulated pending samples into the parent stat under its lock.
+   *
+   * <p>Min and max are drained via individual {@code super.add()} calls so
+   * that {@code MutableStat.minMax} is kept correct. The remaining
+   * {@code n - 2} samples are batched for efficiency. In the rare case where
+   * a concurrent {@code add()} incremented the count before the min/max
+   * accumulators ran (sentinel identity values), the whole batch falls back to
+   * a bulk add; the actual min/max will be captured in the next drain.
+   *
+   * <p>Safe to call from unsynchronized contexts; {@code super.add(long)} is
+   * {@code synchronized(this)}, and Java intrinsic locks are reentrant so
+   * calling this from the already-locked {@link #snapshot} path is fine.
+   */
+  private void drainPending() {
+    long n = pendingCount.sumThenReset();
+    if (n == 0) {
+      return;
+    }
+    long sum = pendingSum.sumThenReset();
+
+    setChanged();
+
+    long min = pendingMin.getThenReset();
+    long max = pendingMax.getThenReset();
+
+    if (min == Long.MAX_VALUE || max == Long.MIN_VALUE) {
+      // Race: count was incremented before accumulate() ran. Fall back to
+      // bulk add; min/max for these items will be captured in the next drain.
+      super.add(n, sum);
+      return;
+    }
+
+    // Drain min and max individually so MutableStat.minMax is updated.
+    super.add(min);
+    if (n > 1) {
+      super.add(max);
+      if (n > 2) {
+        super.add(n - 2, sum - min - max);
+      }
+    }
+  }
+}
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/PerformanceMetrics.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/PerformanceMetrics.java
index eaa17c0278f..c3ea31908d8 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/PerformanceMetrics.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/PerformanceMetrics.java
@@ -23,7 +23,6 @@
 import org.apache.hadoop.metrics2.MetricsRecordBuilder;
 import org.apache.hadoop.metrics2.lib.MetricsRegistry;
 import org.apache.hadoop.metrics2.lib.MutableQuantiles;
-import org.apache.hadoop.metrics2.lib.MutableStat;
 
 /**
  * The {@code PerformanceMetrics} class encapsulates a collection of related
@@ -32,7 +31,7 @@
  * snapshot their values for reporting.
  */
 public class PerformanceMetrics implements Closeable {
-  private final MutableStat stat;
+  private final ConcurrentMutableStat stat;
   private final List<MutableQuantiles> quantiles;
   private final MutableMinMax minMax;
 
@@ -70,7 +69,7 @@ public static synchronized <T> Map<String, 
PerformanceMetrics> initializeMetrics
   public PerformanceMetrics(
       MetricsRegistry registry, String name, String description,
       String sampleName, String valueName, int[] intervals) {
-    stat = registry.newStat(name, description, sampleName, valueName, false);
+    stat = new ConcurrentMutableStat(name, description, sampleName, valueName, 
false);
     quantiles = MetricUtil.createQuantiles(registry, name, description, 
sampleName, valueName, intervals);
     minMax = new MutableMinMax(registry, name, description, valueName);
   }
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/util/ConcurrentMutableStatBenchmark.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/util/ConcurrentMutableStatBenchmark.java
new file mode 100644
index 00000000000..e7417e1a647
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/util/ConcurrentMutableStatBenchmark.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.util;
+
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.LongAdder;
+import org.apache.hadoop.metrics2.lib.MutableStat;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Scalability benchmark comparing {@link MutableStat} and
+ * {@link ConcurrentMutableStat} under two load patterns that model the
+ * thundering-herd observed when many threads release an
+ * {@code OzoneManagerLock} read-lock at the same time.
+ *
+ * <p><b>Burst</b>: all N threads release simultaneously via a
+ * {@link java.util.concurrent.CyclicBarrier}. Reports wall time for the full
+ * burst to complete. {@code MutableStat} serialises the work — burst time
+ * scales linearly with N. {@code ConcurrentMutableStat} uses cell-striped
+ * accumulators — burst time scales sub-linearly.
+ *
+ * <p><b>Steady-state</b>: all N threads call {@code add()} in a tight loop for
+ * a fixed window. Reports total ops/ms. {@code MutableStat} throughput is
+ * capped by the serialised mutex regardless of thread count.
+ * {@code ConcurrentMutableStat} throughput grows with N.
+ *
+ * <p>Each measurement method uses its own concrete stat type so the JIT can
+ * devirtualize {@code add()} monomorphically and eliminate dispatch overhead.
+ *
+ * <p>Run with:
+ * <pre>
+ *   mvn test -pl :hdds-common \
+ *     -Dtest=ConcurrentMutableStatBenchmark \
+ *     -Dgroups=benchmark -Dexcluded-test-groups= \
+ *     -Dsurefire.failIfNoSpecifiedTests=false
+ * </pre>
+ */
+@Tag("benchmark")
+public class ConcurrentMutableStatBenchmark {
+
+  private static final int[] THREAD_COUNTS  = {1, 10, 20, 40, 60, 80};
+
+  // Burst scenario
+  /** Number of {@code add()} calls each thread makes per burst. */
+  private static final int ADDS_PER_BURST   = 500;
+  private static final int WARMUP_BURSTS    = 200;
+  private static final int MEASURE_BURSTS   = 300;
+
+  // Steady-state scenario
+  /** Warm-up window per thread before the measured window starts. */
+  private static final int STEADY_WARMUP_MS  = 200;
+  /** Measurement window per thread for steady-state throughput. */
+  private static final int STEADY_MEASURE_MS = 500;
+  /** Ops per {@code System.nanoTime()} poll to amortise timer-call overhead. 
*/
+  private static final int STEADY_BATCH      = 1_000;
+
+  @Test
+  public void benchmarkBurstScalability() throws Exception {
+    System.out.println();
+    System.out.printf("%-9s  %-24s  %-32s  %s%n",
+        "Threads", "MutableStat µs/burst", "ConcurrentMutableStat µs/burst", 
"Speedup");
+    System.out.println(
+        
"--------------------------------------------------------------------------------------------");
+
+    for (int threads : THREAD_COUNTS) {
+      double baseUs = measureMutableStatBurst(threads);
+      double concUs = measureConcurrentMutableStatBurst(threads);
+      double speedup = baseUs > 0 ? baseUs / concUs : Double.NaN;
+      System.out.printf("%-9d  %-24s  %-32s  %.1fx%n",
+          threads, formatUs(baseUs), formatUs(concUs), speedup);
+    }
+  }
+
+  private static double measureMutableStatBurst(int threads) throws Exception {
+    MutableStat stat = new MutableStat("base", "baseline", "Ops", "Time", 
false);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+    CyclicBarrier barrier = new CyclicBarrier(threads + 1);
+
+    for (int t = 0; t < threads; t++) {
+      pool.submit(() -> {
+        try {
+          while (!Thread.currentThread().isInterrupted()) {
+            barrier.await();
+            for (int i = 0; i < ADDS_PER_BURST; i++) {
+              stat.add(ThreadLocalRandom.current().nextLong(1, 10_000));
+            }
+            barrier.await();
+          }
+        } catch (InterruptedException | BrokenBarrierException e) {
+          Thread.currentThread().interrupt();
+        }
+      });
+    }
+
+    runBursts(barrier, WARMUP_BURSTS);
+    long ns = runBursts(barrier, MEASURE_BURSTS);
+    pool.shutdownNow();
+    pool.awaitTermination(5, TimeUnit.SECONDS);
+    return (double) ns / MEASURE_BURSTS / 1000.0;
+  }
+
+  private static double measureConcurrentMutableStatBurst(int threads) throws 
Exception {
+    ConcurrentMutableStat stat =
+        new ConcurrentMutableStat("conc", "concurrent", "Ops", "Time", false);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+    CyclicBarrier barrier = new CyclicBarrier(threads + 1);
+
+    for (int t = 0; t < threads; t++) {
+      pool.submit(() -> {
+        try {
+          while (!Thread.currentThread().isInterrupted()) {
+            barrier.await();
+            for (int i = 0; i < ADDS_PER_BURST; i++) {
+              stat.add(ThreadLocalRandom.current().nextLong(1, 10_000));
+            }
+            barrier.await();
+          }
+        } catch (InterruptedException | BrokenBarrierException e) {
+          Thread.currentThread().interrupt();
+        }
+      });
+    }
+
+    runBursts(barrier, WARMUP_BURSTS);
+    long ns = runBursts(barrier, MEASURE_BURSTS);
+    pool.shutdownNow();
+    pool.awaitTermination(5, TimeUnit.SECONDS);
+    return (double) ns / MEASURE_BURSTS / 1000.0;
+  }
+
+  @Test
+  public void benchmarkSteadyStateThroughput() throws Exception {
+    System.out.println();
+    System.out.printf("=== Steady-State (continuous load, %d ms window) 
===%n", STEADY_MEASURE_MS);
+    System.out.printf("%-9s  %-24s  %-32s  %s%n",
+        "Threads", "MutableStat ops/ms", "ConcurrentMutableStat ops/ms", 
"Speedup");
+    System.out.println(
+        
"--------------------------------------------------------------------------------------------");
+
+    for (int threads : THREAD_COUNTS) {
+      double baseOps = measureMutableStatSteady(threads);
+      double concOps = measureConcurrentMutableStatSteady(threads);
+      double speedup = baseOps > 0 ? concOps / baseOps : Double.NaN;
+      System.out.printf("%-9d  %-24s  %-32s  %.1fx%n",
+          threads, formatOpsMs(baseOps), formatOpsMs(concOps), speedup);
+    }
+  }
+
+  private static double measureMutableStatSteady(int threads) throws Exception 
{
+    MutableStat stat = new MutableStat("base", "baseline", "Ops", "Time", 
false);
+    LongAdder totalOps = new LongAdder();
+    CountDownLatch start = new CountDownLatch(1);
+    CountDownLatch done  = new CountDownLatch(threads);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+    for (int t = 0; t < threads; t++) {
+      pool.submit(() -> {
+        try {
+          start.await();
+          ThreadLocalRandom rng = ThreadLocalRandom.current();
+          long warmupEnd  = System.nanoTime() + (long) STEADY_WARMUP_MS  * 
1_000_000L;
+          long measureEnd = warmupEnd          + (long) STEADY_MEASURE_MS * 
1_000_000L;
+          while (System.nanoTime() < warmupEnd) {
+            for (int i = 0; i < STEADY_BATCH; i++) {
+              stat.add(rng.nextLong(1, 10_000));
+            }
+          }
+          long ops = 0;
+          while (System.nanoTime() < measureEnd) {
+            for (int i = 0; i < STEADY_BATCH; i++) {
+              stat.add(rng.nextLong(1, 10_000));
+            }
+            ops += STEADY_BATCH;
+          }
+          totalOps.add(ops);
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        } finally {
+          done.countDown();
+        }
+      });
+    }
+    start.countDown();
+    if (!done.await(60, TimeUnit.SECONDS)) {
+      throw new AssertionError("steady-state threads did not finish within 60 
s");
+    }
+    pool.shutdownNow();
+    pool.awaitTermination(5, TimeUnit.SECONDS);
+    return (double) totalOps.sum() / STEADY_MEASURE_MS;
+  }
+
+  private static double measureConcurrentMutableStatSteady(int threads) throws 
Exception {
+    ConcurrentMutableStat stat =
+        new ConcurrentMutableStat("conc", "concurrent", "Ops", "Time", false);
+    LongAdder totalOps = new LongAdder();
+    CountDownLatch start = new CountDownLatch(1);
+    CountDownLatch done  = new CountDownLatch(threads);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+    for (int t = 0; t < threads; t++) {
+      pool.submit(() -> {
+        try {
+          start.await();
+          ThreadLocalRandom rng = ThreadLocalRandom.current();
+          long warmupEnd  = System.nanoTime() + (long) STEADY_WARMUP_MS  * 
1_000_000L;
+          long measureEnd = warmupEnd          + (long) STEADY_MEASURE_MS * 
1_000_000L;
+          while (System.nanoTime() < warmupEnd) {
+            for (int i = 0; i < STEADY_BATCH; i++) {
+              stat.add(rng.nextLong(1, 10_000));
+            }
+          }
+          long ops = 0;
+          while (System.nanoTime() < measureEnd) {
+            for (int i = 0; i < STEADY_BATCH; i++) {
+              stat.add(rng.nextLong(1, 10_000));
+            }
+            ops += STEADY_BATCH;
+          }
+          totalOps.add(ops);
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        } finally {
+          done.countDown();
+        }
+      });
+    }
+    start.countDown();
+    if (!done.await(60, TimeUnit.SECONDS)) {
+      throw new AssertionError("steady-state threads did not finish within 60 
s");
+    }
+    pool.shutdownNow();
+    pool.awaitTermination(5, TimeUnit.SECONDS);
+    return (double) totalOps.sum() / STEADY_MEASURE_MS;
+  }
+
+  private static String formatOpsMs(double opsPerMs) {
+    if (opsPerMs >= 1_000_000) {
+      return String.format("%.2f G ops/ms", opsPerMs / 1_000_000);
+    } else if (opsPerMs >= 1_000) {
+      return String.format("%.2f k ops/ms", opsPerMs / 1_000);
+    }
+    return String.format("%.2f ops/ms", opsPerMs);
+  }
+
+  /**
+   * Runs {@code bursts} rounds through the driver side of the barrier and
+   * returns total elapsed nanoseconds.
+   */
+  private static long runBursts(CyclicBarrier barrier, int bursts) throws 
Exception {
+    long total = 0;
+    for (int i = 0; i < bursts; i++) {
+      long t0 = System.nanoTime();
+      barrier.await();
+      barrier.await();
+      total += System.nanoTime() - t0;
+    }
+    return total;
+  }
+
+  private static String formatUs(double us) {
+    if (us >= 1000.0) {
+      return String.format("%.2f ms", us / 1000.0);
+    }
+    return String.format("%.2f µs", us);
+  }
+}
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/util/TestConcurrentMutableStat.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/util/TestConcurrentMutableStat.java
new file mode 100644
index 00000000000..3ce9239af09
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/util/TestConcurrentMutableStat.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.metrics2.util.SampleStat;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link ConcurrentMutableStat}.
+ */
+public class TestConcurrentMutableStat {
+
+  @Test
+  public void testSingleThreadedCountAndMean() {
+    ConcurrentMutableStat stat = newStat();
+    for (int i = 1; i <= 10; i++) {
+      stat.add(i);  // sum=55, count=10
+    }
+    SampleStat last = stat.lastStat();
+    assertEquals(10, last.numSamples());
+    assertEquals(5.5, last.mean(), 0.001);
+  }
+
+  @Test
+  public void testSingleThreadedMinMax() {
+    ConcurrentMutableStat stat = newStat();
+    stat.add(5);
+    stat.add(1);
+    stat.add(10);
+    stat.add(3);
+    SampleStat last = stat.lastStat();
+    assertEquals(4, last.numSamples());
+    assertEquals(1.0, last.min(), 0.001);
+    assertEquals(10.0, last.max(), 0.001);
+  }
+
+  @Test
+  public void testSingleValueMinEqualsMax() {
+    ConcurrentMutableStat stat = newStat();
+    stat.add(42);
+    SampleStat last = stat.lastStat();
+    assertEquals(1, last.numSamples());
+    assertEquals(42.0, last.min(), 0.001);
+    assertEquals(42.0, last.max(), 0.001);
+  }
+
+  @Test
+  public void testToStringContainsSampleCount() {
+    ConcurrentMutableStat stat = newStat();
+    for (int i = 0; i < 7; i++) {
+      stat.add(100);
+    }
+    assertThat(stat.toString()).contains("Samples = 7");
+  }
+
+  @Test
+  public void testConcurrentAddCount() throws InterruptedException {
+    ConcurrentMutableStat stat = newStat();
+    int threads = 14;
+    int addsPerThread = 1000;
+
+    CountDownLatch start = new CountDownLatch(1);
+    CountDownLatch done = new CountDownLatch(threads);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+
+    for (int t = 0; t < threads; t++) {
+      pool.submit(() -> {
+        try {
+          start.await();
+          for (int i = 0; i < addsPerThread; i++) {
+            stat.add(1);
+          }
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+        } finally {
+          done.countDown();
+        }
+      });
+    }
+    start.countDown();
+    assertTrue(done.await(30, TimeUnit.SECONDS));
+    pool.shutdown();
+
+    assertEquals((long) threads * addsPerThread, stat.lastStat().numSamples());
+  }
+
+  @Test
+  public void testConcurrentAddMaxValue() throws InterruptedException {
+    ConcurrentMutableStat stat = newStat();
+    int threads = 14;
+    CountDownLatch done = new CountDownLatch(threads);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+
+    for (int t = 0; t < threads; t++) {
+      long value = (t + 1) * 10L;  // 10, 20, ..., 140
+      pool.submit(() -> {
+        stat.add(value);
+        done.countDown();
+      });
+    }
+    assertTrue(done.await(10, TimeUnit.SECONDS));
+    pool.shutdown();
+
+    SampleStat last = stat.lastStat();
+    assertEquals(threads, last.numSamples());
+    assertEquals(140.0, last.max(), 0.001);
+  }
+
+  @Test
+  public void testConcurrentAddMinValue() throws InterruptedException {
+    ConcurrentMutableStat stat = newStat();
+    int threads = 14;
+    CountDownLatch done = new CountDownLatch(threads);
+    ExecutorService pool = Executors.newFixedThreadPool(threads);
+
+    for (int t = 0; t < threads; t++) {
+      long value = (t + 1) * 10L;  // 10, 20, ..., 140
+      pool.submit(() -> {
+        stat.add(value);
+        done.countDown();
+      });
+    }
+    assertTrue(done.await(10, TimeUnit.SECONDS));
+    pool.shutdown();
+
+    assertEquals(10.0, stat.lastStat().min(), 0.001);
+  }
+
+  @Test
+  public void testMultipleAddsAndDrains() {
+    ConcurrentMutableStat stat = newStat();
+
+    stat.add(1);
+    stat.add(3);
+    // first drain via toString; interval not reset by toString, samples 
accumulate
+    assertThat(stat.toString()).contains("Samples = 2");
+
+    stat.add(5);
+    stat.add(7);
+    // interval accumulates until snapshot(); all four samples visible
+    assertThat(stat.toString()).contains("Samples = 4");
+    assertEquals(4.0, stat.lastStat().mean(), 0.001);
+    assertEquals(7.0, stat.lastStat().max(), 0.001);
+  }
+
+  private static ConcurrentMutableStat newStat() {
+    return new ConcurrentMutableStat("test", "test stat", "Ops", "Time", true);
+  }
+}
diff --git 
a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockMetrics.java
 
b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockMetrics.java
index 1541819e9a9..0805d533bdd 100644
--- 
a/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockMetrics.java
+++ 
b/hadoop-ozone/interface-storage/src/main/java/org/apache/hadoop/ozone/om/lock/OMLockMetrics.java
@@ -24,9 +24,8 @@
 import org.apache.hadoop.metrics2.MetricsSystem;
 import org.apache.hadoop.metrics2.annotation.Metrics;
 import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
-import org.apache.hadoop.metrics2.lib.MetricsRegistry;
-import org.apache.hadoop.metrics2.lib.MutableStat;
 import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.util.ConcurrentMutableStat;
 
 /**
  * This class is for maintaining the various Ozone Manager Lock Metrics.
@@ -37,26 +36,24 @@ public final class OMLockMetrics implements MetricsSource {
   private static final String SOURCE_NAME =
       OMLockMetrics.class.getSimpleName();
 
-  private final MetricsRegistry registry;
-  private final MutableStat readLockWaitingTimeMsStat;
-  private final MutableStat readLockHeldTimeMsStat;
-  private final MutableStat writeLockWaitingTimeMsStat;
-  private final MutableStat writeLockHeldTimeMsStat;
+  private final ConcurrentMutableStat readLockWaitingTimeMsStat;
+  private final ConcurrentMutableStat readLockHeldTimeMsStat;
+  private final ConcurrentMutableStat writeLockWaitingTimeMsStat;
+  private final ConcurrentMutableStat writeLockHeldTimeMsStat;
 
   private OMLockMetrics() {
-    registry = new MetricsRegistry(SOURCE_NAME);
-    readLockWaitingTimeMsStat = registry.newStat("ReadLockWaitingTime",
+    readLockWaitingTimeMsStat = new 
ConcurrentMutableStat("ReadLockWaitingTime",
         "Time (in milliseconds) spent waiting for acquiring the read lock",
-        "Ops", "Time", true);
-    readLockHeldTimeMsStat = registry.newStat("ReadLockHeldTime",
+        "Ops", "Time", false);
+    readLockHeldTimeMsStat = new ConcurrentMutableStat("ReadLockHeldTime",
         "Time (in milliseconds) spent holding the read lock",
-        "Ops", "Time", true);
-    writeLockWaitingTimeMsStat = registry.newStat("WriteLockWaitingTime",
+        "Ops", "Time", false);
+    writeLockWaitingTimeMsStat = new 
ConcurrentMutableStat("WriteLockWaitingTime",
         "Time (in milliseconds) spent waiting for acquiring the write lock",
-        "Ops", "Time", true);
-    writeLockHeldTimeMsStat = registry.newStat("WriteLockHeldTime",
+        "Ops", "Time", false);
+    writeLockHeldTimeMsStat = new ConcurrentMutableStat("WriteLockHeldTime",
         "Time (in milliseconds) spent holding the write lock",
-        "Ops", "Time", true);
+        "Ops", "Time", false);
   }
 
   /**


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to