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");
+ }
+}