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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new c78add1c7e53 CAMEL-25804: camel-pqc - keep the stateful key gauge 
supplier reachable (#28102)
c78add1c7e53 is described below

commit c78add1c7e53879e27d58d6d9be4aaeda9308c34
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Oct 19 18:51:01 2026 +0200

    CAMEL-25804: camel-pqc - keep the stateful key gauge supplier reachable 
(#28102)
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../pqc/metrics/PQCMicrometerMetrics.java          |  29 +++--
 .../pqc/metrics/PQCMicrometerMetricsTest.java      | 126 +++++++++++++++++++++
 2 files changed, 146 insertions(+), 9 deletions(-)

diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
index 63d20d3642a5..f94978dde075 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetrics.java
@@ -29,6 +29,11 @@ import org.apache.camel.CamelContext;
 /**
  * Micrometer-backed {@link PQCMetrics}. All {@code io.micrometer} references 
live in this class so that it is only
  * loaded when Micrometer is actually present on the classpath (see {@code 
PQCProducer#createMetrics}).
+ * <p/>
+ * Note on {@link #close()}: a {@code MeterRegistry} is shared across the JVM 
and identifies a meter by its id, so two
+ * producers whose operation, algorithm and outcome tags coincide share one 
meter instance. Closing one therefore
+ * de-registers a meter the other is still using. Giving each producer a 
distinguishing tag would fix that, but it
+ * changes the published tag set, so it is left alone here.
  */
 public class PQCMicrometerMetrics implements PQCMetrics {
 
@@ -37,7 +42,11 @@ public class PQCMicrometerMetrics implements PQCMetrics {
 
     private final MeterRegistry registry;
     private final ConcurrentMap<String, Counter> counters = new 
ConcurrentHashMap<>();
-    private volatile Gauge statefulKeyGauge;
+    private final ConcurrentMap<String, Gauge> statefulKeyGauges = new 
ConcurrentHashMap<>();
+    // Micrometer holds the state object of a gauge through a WeakReference, 
so the suppliers have to be kept
+    // reachable here. Without this the gauge reports NaN as soon as the 
lambda is collected - and this particular
+    // gauge is the one that warns an operator a one-time-use key is running 
out of signatures.
+    private final ConcurrentMap<String, LongSupplier> statefulKeySuppliers = 
new ConcurrentHashMap<>();
 
     public PQCMicrometerMetrics(MeterRegistry registry) {
         this.registry = registry;
@@ -70,19 +79,21 @@ public class PQCMicrometerMetrics implements PQCMetrics {
 
     @Override
     public void registerStatefulKeyGauge(String algorithm, LongSupplier 
remainingSupplier) {
-        statefulKeyGauge = Gauge.builder(METRIC_STATEFUL_REMAINING, 
remainingSupplier, LongSupplier::getAsLong)
-                .tag("algorithm", algorithm)
-                .description("Remaining signatures for the stateful PQC 
signature key")
-                .register(registry);
+        // Held for the lifetime of this instance so Micrometer's weak 
reference to it stays resolvable
+        statefulKeySuppliers.put(algorithm, remainingSupplier);
+        statefulKeyGauges.put(algorithm,
+                Gauge.builder(METRIC_STATEFUL_REMAINING, remainingSupplier, 
LongSupplier::getAsLong)
+                        .tag("algorithm", algorithm)
+                        .description("Remaining signatures for the stateful 
PQC signature key")
+                        .register(registry));
     }
 
     @Override
     public void close() {
         counters.values().forEach(registry::remove);
         counters.clear();
-        if (statefulKeyGauge != null) {
-            registry.remove(statefulKeyGauge);
-            statefulKeyGauge = null;
-        }
+        statefulKeyGauges.values().forEach(registry::remove);
+        statefulKeyGauges.clear();
+        statefulKeySuppliers.clear();
     }
 }
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetricsTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetricsTest.java
new file mode 100644
index 000000000000..02a6cfa34ac0
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/metrics/PQCMicrometerMetricsTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.camel.component.pqc.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.atomic.AtomicLong;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * Micrometer holds the state object of a gauge through a weak reference, so 
the remaining-signatures supplier has to be
+ * kept reachable by this class or the gauge silently degrades to NaN.
+ */
+class PQCMicrometerMetricsTest {
+
+    @Test
+    void theStatefulGaugeKeepsReportingAfterTheSupplierBecomesUnreachable() {
+        MeterRegistry registry = new SimpleMeterRegistry();
+        PQCMicrometerMetrics metrics = new PQCMicrometerMetrics(registry);
+
+        AtomicLong remaining = new AtomicLong(42);
+        // the lambda is reachable only through the metrics instance from here 
on
+        metrics.registerStatefulKeyGauge("LMS", remaining::get);
+
+        forceGc();
+
+        assertEquals(42.0,
+                
registry.get(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm", 
"LMS").gauge().value(),
+                "the gauge must still read the supplier after a collection");
+
+        remaining.set(7);
+        assertEquals(7.0,
+                
registry.get(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm", 
"LMS").gauge().value(),
+                "the gauge must track the live value, not a snapshot");
+    }
+
+    @Test
+    void registeringASecondGaugeDoesNotOrphanTheFirst() {
+        MeterRegistry registry = new SimpleMeterRegistry();
+        PQCMicrometerMetrics metrics = new PQCMicrometerMetrics(registry);
+
+        metrics.registerStatefulKeyGauge("LMS", () -> 10);
+        metrics.registerStatefulKeyGauge("XMSS", () -> 20);
+
+        assertEquals(10.0,
+                
registry.get(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm", 
"LMS").gauge().value());
+        assertEquals(20.0,
+                
registry.get(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm", 
"XMSS").gauge().value());
+
+        metrics.close();
+
+        
assertNull(registry.find(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm",
 "LMS").gauge(),
+                "close() must remove every gauge it registered, not only the 
last one");
+        
assertNull(registry.find(PQCMicrometerMetrics.METRIC_STATEFUL_REMAINING).tag("algorithm",
 "XMSS").gauge());
+    }
+
+    @Test
+    void operationCountersAreRecordedAndRemovedOnClose() {
+        MeterRegistry registry = new SimpleMeterRegistry();
+        PQCMicrometerMetrics metrics = new PQCMicrometerMetrics(registry);
+
+        metrics.recordOperation("sign", "MLDSA", true);
+        metrics.recordOperation("sign", "MLDSA", true);
+        metrics.recordOperation("sign", "MLDSA", false);
+
+        assertEquals(2.0, registry.get(PQCMicrometerMetrics.METRIC_OPERATIONS)
+                .tag("operation", "sign").tag("algorithm", 
"MLDSA").tag("outcome", "success").counter().count());
+        assertEquals(1.0, registry.get(PQCMicrometerMetrics.METRIC_OPERATIONS)
+                .tag("operation", "sign").tag("algorithm", 
"MLDSA").tag("outcome", "failure").counter().count());
+
+        metrics.close();
+
+        
assertNull(registry.find(PQCMicrometerMetrics.METRIC_OPERATIONS).counter());
+    }
+
+    @Test
+    void aContextWithoutAMeterRegistryYieldsTheNoopImplementation() throws 
Exception {
+        try (CamelContext context = new DefaultCamelContext()) {
+            context.start();
+
+            PQCMetrics metrics = PQCMicrometerMetrics.create(context);
+
+            assertSame(PQCMetrics.NOOP, metrics, "with no MeterRegistry in the 
registry the no-op must be returned");
+            // the no-op must tolerate every call without a registry behind it
+            metrics.recordOperation("sign", "MLDSA", true);
+            metrics.registerStatefulKeyGauge("LMS", () -> 1);
+            metrics.close();
+        }
+    }
+
+    /**
+     * Collects until a canary weak reference is actually cleared, so the 
assertion that follows is about the gauge
+     * rather than about whether a collection happened to run.
+     */
+    private static void forceGc() {
+        WeakReference<Object> canary = new WeakReference<>(new Object());
+        for (int i = 0; i < 100 && canary.get() != null; i++) {
+            System.gc();
+        }
+        // -XX:+DisableExplicitGC makes System.gc() a no-op; the assertion 
below would then prove nothing
+        assumeTrue(canary.get() == null, "no garbage collection was observed, 
skipping");
+    }
+}

Reply via email to