This is an automated email from the ASF dual-hosted git repository.
nodece pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new c240dd2ea2b8 [fix][broker] Fix bucket delayed message index metrics
reset on scrape (#26171)
c240dd2ea2b8 is described below
commit c240dd2ea2b8365dff921190c5af8ba7380914f6
Author: Zixuan Liu <[email protected]>
AuthorDate: Fri Jul 10 10:41:18 2026 +0800
[fix][broker] Fix bucket delayed message index metrics reset on scrape
(#26171)
---
.../bookkeeper/mledger/util/StatsBuckets.java | 28 +++++++++
.../bucket/BucketDelayedMessageIndexStats.java | 8 +--
.../bucket/BucketDelayedMessageIndexStatsTest.java | 68 ++++++++++++++++++++++
3 files changed, 100 insertions(+), 4 deletions(-)
diff --git
a/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/util/StatsBuckets.java
b/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/util/StatsBuckets.java
index 60c0a7f6c9d2..ec3a7e00fd39 100644
---
a/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/util/StatsBuckets.java
+++
b/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/util/StatsBuckets.java
@@ -60,6 +60,16 @@ public class StatsBuckets {
sumCounter.add(value);
}
+ /**
+ * Snapshots the current values into {@link #getBuckets()}, {@link
#getCount()}, and
+ * {@link #getSum()}, then resets the internal counters to zero.
+ *
+ * <p>Use this for periodic aggregation where the consumer reads values on
a fixed
+ * schedule (e.g. {@code ManagedLedgerMBeanImpl} refreshes every
+ * {@code managedLedgerStatsPeriodSeconds}). Do <b>not</b> use this for
metrics that
+ * are scraped on demand (e.g. Prometheus {@code /metrics}), because data
recorded
+ * between scrapes is lost after each call — use {@link #snapshot()}
instead.
+ */
public void refresh() {
long count = 0;
sum = sumCounter.sumThenReset();
@@ -73,6 +83,24 @@ public class StatsBuckets {
this.count = count;
}
+ /**
+ * Snapshots the current cumulative values without resetting the internal
counters.
+ * Use this instead of {@link #refresh()} when the data should survive
across reads
+ * (e.g. Prometheus metrics that are scraped multiple times).
+ */
+ public void snapshot() {
+ long count = 0;
+ sum = sumCounter.sum();
+
+ for (int i = 0; i < buckets.length; i++) {
+ long value = buckets[i].sum();
+ count += value;
+ values[i] = value;
+ }
+
+ this.count = count;
+ }
+
public void reset() {
sum = 0;
sumCounter.reset();
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStats.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStats.java
index 68788c359d56..b9e6c7dc64c7 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStats.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStats.java
@@ -47,8 +47,8 @@ public class BucketDelayedMessageIndexStats {
private static final String BUCKET_TOTAL_NAME =
"pulsar_delayed_message_index_bucket_total";
private static final String INDEX_LOADED_NAME =
"pulsar_delayed_message_index_loaded";
private static final String SNAPSHOT_SIZE_BYTES_NAME =
"pulsar_delayed_message_index_bucket_snapshot_size_bytes";
- private static final String OP_COUNT_NAME =
"pulsar_delayed_message_index_bucket_op_count";
- private static final String OP_LATENCY_NAME =
"pulsar_delayed_message_index_bucket_op_latency_ms";
+ static final String OP_COUNT_NAME =
"pulsar_delayed_message_index_bucket_op_count";
+ static final String OP_LATENCY_NAME =
"pulsar_delayed_message_index_bucket_op_latency_ms";
private final AtomicInteger delayedMessageIndexBucketTotal = new
AtomicInteger();
private final AtomicLong delayedMessageIndexLoaded = new AtomicLong();
@@ -75,11 +75,11 @@ public class BucketDelayedMessageIndexStats {
String[] labels = splitKey(k);
String[] labelsAndValues = new String[] {"state", labels[0],
"type", labels[1]};
String key = OP_COUNT_NAME + joinKey(labelsAndValues);
- metrics.put(key, new TopicMetricBean(OP_COUNT_NAME,
count.sumThenReset(), labelsAndValues));
+ metrics.put(key, new TopicMetricBean(OP_COUNT_NAME, count.sum(),
labelsAndValues));
});
delayedMessageIndexBucketOpLatencyMs.forEach((typeName, statsBuckets)
-> {
- statsBuckets.refresh();
+ statsBuckets.snapshot();
long[] buckets = statsBuckets.getBuckets();
for (int i = 0; i < buckets.length; i++) {
long count = buckets[i];
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStatsTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStatsTest.java
new file mode 100644
index 000000000000..b1b7d8248cc3
--- /dev/null
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/delayed/bucket/BucketDelayedMessageIndexStatsTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.pulsar.broker.delayed.bucket;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import java.util.Map;
+import org.apache.pulsar.common.policies.data.stats.TopicMetricBean;
+import org.testng.annotations.Test;
+
+public class BucketDelayedMessageIndexStatsTest {
+
+ private static final String OP_COUNT =
BucketDelayedMessageIndexStats.OP_COUNT_NAME;
+ private static final String OP_LATENCY_COUNT =
BucketDelayedMessageIndexStats.OP_LATENCY_NAME + "_count";
+ private static final String OP_LATENCY_SUM =
BucketDelayedMessageIndexStats.OP_LATENCY_NAME + "_sum";
+
+ @Test
+ public void testMetricsAreCumulativeAcrossScrapes() {
+ BucketDelayedMessageIndexStats stats = new
BucketDelayedMessageIndexStats();
+ var type = BucketDelayedMessageIndexStats.Type.create;
+
+ stats.recordTriggerEvent(type);
+ stats.recordSuccessEvent(type, 200);
+ stats.recordSuccessEvent(type, 300);
+
+ // First scrape
+ var m1 = stats.genTopicMetricMap();
+ assertEquals(get(m1, OP_COUNT, "state", "succeed", "type", "create"),
2);
+ assertEquals(get(m1, OP_LATENCY_COUNT, "type", "create"), 2);
+ assertEquals(get(m1, OP_LATENCY_SUM, "type", "create"), 500);
+
+ // Second scrape — must not reset
+ var m2 = stats.genTopicMetricMap();
+ assertEquals(get(m2, OP_COUNT, "state", "succeed", "type", "create"),
2);
+ assertEquals(get(m2, OP_LATENCY_COUNT, "type", "create"), 2);
+ assertEquals(get(m2, OP_LATENCY_SUM, "type", "create"), 500);
+
+ // Third event — cumulative
+ stats.recordSuccessEvent(type, 100);
+ var m3 = stats.genTopicMetricMap();
+ assertEquals(get(m3, OP_COUNT, "state", "succeed", "type", "create"),
3);
+ assertEquals(get(m3, OP_LATENCY_COUNT, "type", "create"), 3);
+ assertEquals(get(m3, OP_LATENCY_SUM, "type", "create"), 600);
+ }
+
+ private static long get(Map<String, TopicMetricBean> metrics, String name,
String... labelsAndValues) {
+ String key = name +
BucketDelayedMessageIndexStats.joinKey(labelsAndValues);
+ TopicMetricBean bean = metrics.get(key);
+ assertNotNull(bean, "Metric not found: " + key);
+ return (long) bean.value;
+ }
+}