This is an automated email from the ASF dual-hosted git repository.
apolovtsev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new a1df12f1a6c IGNITE-28230 Implement SimpleMetricSource (#7855)
a1df12f1a6c is described below
commit a1df12f1a6c5e43af6af857100b4be4266eea751
Author: Viacheslav Blinov <[email protected]>
AuthorDate: Wed Apr 1 12:01:54 2026 +0300
IGNITE-28230 Implement SimpleMetricSource (#7855)
---
.../internal/metrics/AbstractMetricSource.java | 2 +
.../internal/metrics/SimpleMetricSource.java | 211 ++++++++++++++++
.../internal/metrics/SimpleMetricSourceTest.java | 274 +++++++++++++++++++++
3 files changed, 487 insertions(+)
diff --git
a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/AbstractMetricSource.java
b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/AbstractMetricSource.java
index 2e3115cb782..4c199efc552 100644
---
a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/AbstractMetricSource.java
+++
b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/AbstractMetricSource.java
@@ -26,7 +26,9 @@ import org.jetbrains.annotations.Nullable;
* Base class for all metric sources.
*
* @param <T> Holder type.
+ * @deprecated Use {@link SimpleMetricSource} instead.
*/
+@Deprecated
public abstract class AbstractMetricSource<T extends
AbstractMetricSource.Holder<T>> implements MetricSource {
/** Holder field updater. */
@SuppressWarnings("rawtypes")
diff --git
a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/SimpleMetricSource.java
b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/SimpleMetricSource.java
new file mode 100644
index 00000000000..f0c26c5ce3a
--- /dev/null
+++
b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/SimpleMetricSource.java
@@ -0,0 +1,211 @@
+/*
+ * 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.ignite.internal.metrics;
+
+import java.util.HashMap;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+import java.util.function.DoubleSupplier;
+import java.util.function.IntSupplier;
+import java.util.function.LongSupplier;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A {@link MetricSource} with factory methods for creating and registering
metrics.
+ *
+ * <p>Metrics are created via factory methods and stored in a {@link
ConcurrentHashMap}.
+ * Enable/disable controls registry visibility only — metrics themselves are
always alive
+ * and values persist across enable/disable cycles.
+ *
+ * <p>Usage:
+ * <pre>{@code
+ * public class MyMetrics {
+ * private final LongAdderMetric requests;
+ * private final DistributionMetric duration;
+ *
+ * public MyMetrics(SimpleMetricSource source) {
+ * requests = source.longAdder("Requests", "Total requests.");
+ * duration = source.distribution("Duration", "Request duration in
ms.",
+ * new long[]{1, 5, 10, 50, 100, 500});
+ * }
+ *
+ * public void onRequest(long durationMs) {
+ * requests.increment();
+ * duration.add(durationMs);
+ * }
+ * }
+ * }</pre>
+ */
+public final class SimpleMetricSource implements MetricSource {
+ @SuppressWarnings("rawtypes")
+ private static final AtomicReferenceFieldUpdater<SimpleMetricSource,
MetricSet> METRIC_SET_UPD =
+ AtomicReferenceFieldUpdater.newUpdater(SimpleMetricSource.class,
MetricSet.class, "metricSet");
+
+ private final String name;
+
+ private final String description;
+
+ private final @Nullable String group;
+
+ private final ConcurrentMap<String, Metric> metrics = new
ConcurrentHashMap<>();
+
+ /** Non-null when enabled. */
+ private volatile @Nullable MetricSet metricSet;
+
+ /**
+ * Constructor.
+ *
+ * @param name Metric source name.
+ * @param description Metric source description.
+ */
+ public SimpleMetricSource(String name, String description) {
+ this(name, description, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param name Metric source name.
+ * @param description Metric source description.
+ * @param group Optional group name for additional grouping in external
systems (e.g. JMX).
+ */
+ public SimpleMetricSource(String name, String description, @Nullable
String group) {
+ this.name = Objects.requireNonNull(name, "name");
+ this.description = Objects.requireNonNull(description, "description");
+ this.group = group;
+ }
+
+ @Override
+ public final String name() {
+ return name;
+ }
+
+ @Override
+ public String description() {
+ return description;
+ }
+
+ @Override
+ public @Nullable String group() {
+ return group;
+ }
+
+ /** Creates an {@link AtomicIntMetric}. */
+ public AtomicIntMetric atomicInt(String name, @Nullable String
description) {
+ return register(new AtomicIntMetric(name, description));
+ }
+
+ /** Creates an {@link IntGauge}. */
+ public IntGauge intGauge(String name, @Nullable String description,
IntSupplier supplier) {
+ return register(new IntGauge(name, description, supplier));
+ }
+
+ /** Creates an {@link AtomicLongMetric}. */
+ public AtomicLongMetric atomicLong(String name, @Nullable String
description) {
+ return register(new AtomicLongMetric(name, description));
+ }
+
+ /** Creates a {@link LongAdderMetric}. */
+ public LongAdderMetric longAdder(String name, @Nullable String
description) {
+ return register(new LongAdderMetric(name, description));
+ }
+
+ /** Creates a {@link LongGauge}. */
+ public LongGauge longGauge(String name, @Nullable String description,
LongSupplier supplier) {
+ return register(new LongGauge(name, description, supplier));
+ }
+
+ /** Creates an {@link AtomicDoubleMetric}. */
+ public AtomicDoubleMetric atomicDouble(String name, @Nullable String
description) {
+ return register(new AtomicDoubleMetric(name, description));
+ }
+
+ /** Creates a {@link DoubleAdderMetric}. */
+ public DoubleAdderMetric doubleAdder(String name, @Nullable String
description) {
+ return register(new DoubleAdderMetric(name, description));
+ }
+
+ /** Creates a {@link DoubleGauge}. */
+ public DoubleGauge doubleGauge(String name, @Nullable String description,
DoubleSupplier supplier) {
+ return register(new DoubleGauge(name, description, supplier));
+ }
+
+ /** Creates a {@link HitRateMetric} with default counters array size. */
+ public HitRateMetric hitRate(String name, @Nullable String description,
long rateTimeInterval) {
+ return register(new HitRateMetric(name, description,
rateTimeInterval));
+ }
+
+ /** Creates a {@link HitRateMetric} with custom counters array size. */
+ public HitRateMetric hitRate(String name, @Nullable String description,
long rateTimeInterval, int size) {
+ return register(new HitRateMetric(name, description, rateTimeInterval,
size));
+ }
+
+ /** Creates a {@link DistributionMetric}. Bounds must be sorted, unique,
and start from {@code >= 0}. */
+ public DistributionMetric distribution(String name, @Nullable String
description, long[] bounds) {
+ return register(new DistributionMetric(name, description, bounds));
+ }
+
+ /**
+ * Registers a pre-created metric. Use this for types without a dedicated
factory method
+ * ({@link StringGauge}, {@link UuidGauge}, etc.).
+ *
+ * @return The same metric instance.
+ * @throws IllegalStateException If a metric with the given name is
already registered.
+ */
+ public <T extends Metric> T register(T metric) {
+ Objects.requireNonNull(metric, "metric");
+
+ Metric existing = metrics.putIfAbsent(metric.name(), metric);
+
+ if (existing != null) {
+ throw new IllegalStateException("Metric with given name is already
registered [sourceName="
+ + this.name + ", metricName=" + metric.name() + ']');
+ }
+
+ return metric;
+ }
+
+ @Override
+ public @Nullable MetricSet enable() {
+ if (metricSet != null) {
+ return null;
+ }
+
+ // TODO: IGNITE-28412 consider making MetricSet observable instead of
a single snapshot to support post-enable metric addition.
+ MetricSet newMetricSet = new MetricSet(name, description, group, new
HashMap<>(metrics));
+
+ if (METRIC_SET_UPD.compareAndSet(this, null, newMetricSet)) {
+ return newMetricSet;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void disable() {
+ METRIC_SET_UPD.set(this, null);
+ }
+
+ @Override
+ public boolean enabled() {
+ return metricSet != null;
+ }
+
+}
diff --git
a/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/SimpleMetricSourceTest.java
b/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/SimpleMetricSourceTest.java
new file mode 100644
index 00000000000..189e23e1809
--- /dev/null
+++
b/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/SimpleMetricSourceTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.ignite.internal.metrics;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SimpleMetricSource}.
+ */
+public class SimpleMetricSourceTest {
+ private static final String SOURCE_NAME = "test.source";
+
+ private static SimpleMetricSource createSource() {
+ return new SimpleMetricSource(SOURCE_NAME, "Test source.");
+ }
+
+ private static SimpleMetricSource createEnabledSource() {
+ SimpleMetricSource source = createSource();
+ source.enable();
+ return source;
+ }
+
+ // -- Constructor and properties --
+
+ @Test
+ void properties() {
+ SimpleMetricSource source = new SimpleMetricSource("name", "desc",
"group");
+
+ assertEquals("name", source.name());
+ assertEquals("desc", source.description());
+ assertEquals("group", source.group());
+ }
+
+ @Test
+ void groupDefaultsToNull() {
+ SimpleMetricSource source = new SimpleMetricSource("name", "desc");
+
+ assertNull(source.group());
+ }
+
+ @Test
+ void nullNameThrows() {
+ assertThrows(NullPointerException.class, () -> new
SimpleMetricSource(null, "desc"));
+ }
+
+ @Test
+ void nullDescriptionThrows() {
+ assertThrows(NullPointerException.class, () -> new
SimpleMetricSource("name", null));
+ }
+
+ // -- Factory methods --
+
+ @Test
+ void factoryMethodsCreateAllMetricTypes() {
+ SimpleMetricSource source = createEnabledSource();
+
+ assertNotNull(source.atomicInt("ai", null));
+ assertNotNull(source.atomicLong("al", null));
+ assertNotNull(source.longAdder("la", null));
+ assertNotNull(source.atomicDouble("ad", null));
+ assertNotNull(source.doubleAdder("da", null));
+ assertNotNull(source.intGauge("ig", null, () -> 0));
+ assertNotNull(source.longGauge("lg", null, () -> 0L));
+ assertNotNull(source.doubleGauge("dg", null, () -> 0.0));
+ assertNotNull(source.hitRate("hr", null, 1000));
+ assertNotNull(source.hitRate("hr2", null, 1000, 5));
+ assertNotNull(source.distribution("dist", null, new long[]{10, 100}));
+ }
+
+ @Test
+ void registerReturnsSameInstance() {
+ SimpleMetricSource source = createSource();
+ StringGauge gauge = new StringGauge("id", null, () -> "val");
+
+ assertSame(gauge, source.register(gauge));
+ }
+
+ @Test
+ void duplicateMetricNameThrows() {
+ SimpleMetricSource source = createSource();
+ source.atomicLong("Counter", null);
+
+ IllegalStateException ex = assertThrows(
+ IllegalStateException.class,
+ () -> source.atomicLong("Counter", null)
+ );
+
+ assertTrue(ex.getMessage().contains(SOURCE_NAME));
+ assertTrue(ex.getMessage().contains("Counter"));
+ }
+
+ @Test
+ void nullMetricThrows() {
+ assertThrows(NullPointerException.class, () ->
createSource().register(null));
+ }
+
+ // -- Enable/disable lifecycle --
+
+ @Test
+ void notEnabledByDefault() {
+ assertFalse(createSource().enabled());
+ }
+
+ @Test
+ void enableReturnsMetricSet() {
+ SimpleMetricSource source = createSource();
+ LongAdderMetric counter = source.longAdder("Counter", null);
+
+ MetricSet metricSet = source.enable();
+
+ assertNotNull(metricSet);
+ assertTrue(source.enabled());
+ assertSame(counter, metricSet.get("Counter"));
+ }
+
+ @Test
+ void secondEnableReturnsNull() {
+ SimpleMetricSource source = createSource();
+ source.enable();
+
+ assertNull(source.enable());
+ }
+
+ @Test
+ void disableWorks() {
+ SimpleMetricSource source = createSource();
+ source.enable();
+ source.disable();
+
+ assertFalse(source.enabled());
+ }
+
+ @Test
+ void disableWhenAlreadyDisabledIsNoOp() {
+ SimpleMetricSource source = createSource();
+ source.disable();
+
+ assertFalse(source.enabled());
+ }
+
+ @Test
+ void enableEmptySource() {
+ MetricSet metricSet = createEnabledSource().enable();
+
+ // Already enabled, returns null.
+ assertNull(metricSet);
+ }
+
+ // -- Metrics record regardless of enabled state --
+
+ @Test
+ void metricsRecordWhenDisabled() {
+ SimpleMetricSource source = createSource();
+
+ LongAdderMetric counter = source.longAdder("Counter", null);
+ AtomicIntMetric intCounter = source.atomicInt("IntCounter", null);
+ DistributionMetric dist = source.distribution("Dist", null, new
long[]{10});
+ HitRateMetric hitRate = source.hitRate("Rate", null, 60_000);
+
+ assertFalse(source.enabled());
+
+ counter.increment();
+ intCounter.increment();
+ dist.add(5);
+ hitRate.increment();
+
+ assertEquals(1L, counter.value());
+ assertEquals(1, intCounter.value());
+ assertEquals(1L, dist.value()[0]);
+ }
+
+ @Test
+ void gaugesWorkRegardlessOfEnabledState() {
+ SimpleMetricSource source = createSource();
+ int[] holder = {42};
+
+ IntGauge gauge = source.intGauge("Gauge", null, () -> holder[0]);
+
+ assertEquals(42, gauge.value());
+
+ source.enable();
+ holder[0] = 99;
+
+ assertEquals(99, gauge.value());
+ }
+
+ // -- Value persistence across cycles --
+
+ @Test
+ void valuesPreservedAcrossEnableDisableCycle() {
+ SimpleMetricSource source = createEnabledSource();
+
+ LongAdderMetric counter = source.longAdder("Counter", null);
+ counter.add(100);
+
+ source.disable();
+ MetricSet secondSet = source.enable();
+
+ assertSame(counter, secondSet.get("Counter"));
+ assertEquals(100L, counter.value());
+ }
+
+ // -- Post-enable metric addition --
+
+ @Test
+ void metricAddedAfterEnableIsFunctional() {
+ SimpleMetricSource source = createEnabledSource();
+
+ AtomicLongMetric late = source.atomicLong("Late", null);
+ late.increment();
+
+ assertTrue(source.enabled());
+ assertEquals(1L, late.value());
+ }
+
+ // -- Registry integration --
+
+ @Test
+ void worksWithMetricRegistry() {
+ MetricRegistry registry = new MetricRegistry();
+ SimpleMetricSource source = createSource();
+ LongAdderMetric counter = source.longAdder("Requests", null);
+
+ registry.registerSource(source);
+ MetricSet metricSet = registry.enable(SOURCE_NAME);
+
+ assertNotNull(metricSet);
+ assertSame(counter, metricSet.get("Requests"));
+
+ counter.increment();
+ assertEquals(1L, ((LongAdderMetric)
metricSet.get("Requests")).value());
+ }
+
+ @Test
+ void registryDisableAndReEnable() {
+ MetricRegistry registry = new MetricRegistry();
+ SimpleMetricSource source = createSource();
+ LongAdderMetric counter = source.longAdder("Requests", null);
+
+ registry.registerSource(source);
+ registry.enable(SOURCE_NAME);
+ counter.add(10);
+
+ registry.disable(SOURCE_NAME);
+ assertFalse(source.enabled());
+
+ MetricSet reEnabled = registry.enable(source);
+ assertNotNull(reEnabled);
+ assertEquals(10L, ((LongAdderMetric)
reEnabled.get("Requests")).value());
+ }
+}