From 00f4733f2821742336ca197c7b86a8316fc7189a Mon Sep 17 00:00:00 2001 From: Logic Date: Thu, 30 Jul 2026 16:20:37 +0800 Subject: [PATCH 1/2] maintenance: canonicalize alert label fingerprints --- .../hertzbeat/alert/util/AlertUtil.java | 24 +++++--- .../hertzbeat/alert/util/AlertUtilTest.java | 61 +++++++++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java index a1ed180c3f8..6620c98843a 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java @@ -17,10 +17,8 @@ package org.apache.hertzbeat.alert.util; -import java.util.Arrays; -import java.util.List; +import java.util.Comparator; import java.util.Map; -import java.util.Objects; /** * alert util @@ -32,9 +30,21 @@ public class AlertUtil { * @param fingerPrints finger prints */ public static String calculateFingerprint(Map fingerPrints) { - List keyList = fingerPrints.keySet().stream().filter(Objects::nonNull).sorted().toList(); - List valueList = fingerPrints.values().stream().filter(Objects::nonNull).sorted().toList(); - return Arrays.hashCode(keyList.toArray(new String[0])) + "-" - + Arrays.hashCode(valueList.toArray(new String[0])); + StringBuilder canonicalLabels = new StringBuilder(); + fingerPrints.entrySet().stream() + .sorted(Map.Entry.comparingByKey(Comparator.nullsFirst(Comparator.naturalOrder()))) + .forEach(entry -> { + appendLengthPrefixed(canonicalLabels, entry.getKey()); + appendLengthPrefixed(canonicalLabels, entry.getValue()); + }); + return CryptoUtils.sha256Hex(canonicalLabels.toString()); + } + + private static void appendLengthPrefixed(StringBuilder target, String value) { + if (value == null) { + target.append("-1:"); + return; + } + target.append(value.length()).append(':').append(value); } } diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java new file mode 100644 index 00000000000..b6347130684 --- /dev/null +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertUtilTest.java @@ -0,0 +1,61 @@ +/* + * 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.hertzbeat.alert.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AlertUtil}. + */ +class AlertUtilTest { + + @Test + void calculateFingerprintPreservesLabelPairing() { + Map first = new LinkedHashMap<>(); + first.put("environment", "production"); + first.put("team", "payments"); + + Map swapped = new LinkedHashMap<>(); + swapped.put("environment", "payments"); + swapped.put("team", "production"); + + assertNotEquals( + AlertUtil.calculateFingerprint(first), + AlertUtil.calculateFingerprint(swapped)); + } + + @Test + void calculateFingerprintIsIndependentOfMapIterationOrder() { + Map first = new LinkedHashMap<>(); + first.put("environment", "production"); + first.put("team", "payments"); + + Map reversed = new LinkedHashMap<>(); + reversed.put("team", "payments"); + reversed.put("environment", "production"); + + assertEquals( + AlertUtil.calculateFingerprint(first), + AlertUtil.calculateFingerprint(reversed)); + } +} From 2c5522acb3a214254a51c0dd55a69a6a6ce82e21 Mon Sep 17 00:00:00 2001 From: Logic Date: Thu, 30 Jul 2026 21:31:57 +0800 Subject: [PATCH 2/2] document alert cache identity compatibility --- .../hertzbeat/alert/util/AlertUtil.java | 10 +++++-- .../calculate/AlarmCacheManagerTest.java | 27 ++++++++++++++++++- .../alert/reduce/AlarmCommonReduceTest.java | 24 +++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java index 6620c98843a..06cc39600a4 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertUtil.java @@ -26,8 +26,14 @@ public class AlertUtil { /** - * calculate fingerprint - * @param fingerPrints finger prints + * Calculate an in-memory alert cache coordinate. + * + *

This value is rebuilt from persisted alert labels when the process + * starts. It is not the durable {@code SingleAlert.fingerprint} used by + * persistence, grouping, silence, or inhibition.

+ * + * @param fingerPrints labels used by the calculator cache + * @return deterministic cache coordinate */ public static String calculateFingerprint(Map fingerPrints) { StringBuilder canonicalLabels = new StringBuilder(); diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java index 95a43ffbfa0..c2a76092e35 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/calculate/AlarmCacheManagerTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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.mockito.Mockito.when; /** @@ -132,4 +133,28 @@ void testHistorical() { historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint); assertNull(historicalSingleAlert); } -} \ No newline at end of file + + @Test + void restartShouldRebuildCacheKeyWithoutChangingPersistedFingerprint() { + Map labels = Map.of( + CommonConstants.LABEL_DEFINE_ID, "7", + CommonConstants.LABEL_ALERT_NAME, "disk_full", + "instance", "db-1"); + SingleAlert persistedAlert = SingleAlert.builder() + .id(99L) + .fingerprint("alertname:disk_full,define_id:7,instance:db-1") + .labels(labels) + .status(CommonConstants.ALERT_STATUS_FIRING) + .build(); + when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)) + .thenReturn(Collections.singletonList(persistedAlert)); + + alarmCacheManager = new AlarmCacheManager(singleAlertDao); + String rebuiltCacheKey = AlertUtil.calculateFingerprint(labels); + SingleAlert resolved = alarmCacheManager.removeFiring(7L, rebuiltCacheKey); + + assertSame(persistedAlert, resolved); + assertEquals("alertname:disk_full,define_id:7,instance:db-1", resolved.getFingerprint()); + assertNull(alarmCacheManager.getFiring(7L, rebuiltCacheKey)); + } +} diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java index 02081885293..bd8cdfe217f 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduceTest.java @@ -17,6 +17,7 @@ package org.apache.hertzbeat.alert.reduce; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -28,6 +29,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hertzbeat.common.entity.alerter.SingleAlert; import org.apache.hertzbeat.common.config.VirtualThreadProperties; import org.junit.jupiter.api.AfterEach; @@ -85,6 +87,28 @@ void testReduceAndSendAlarmRunsOnVirtualThread() throws Exception { assertTrue(virtualThread.get()); } + @Test + void durableFingerprintShouldRemainIndependentFromCalculatorCacheKey() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference durableFingerprint = new AtomicReference<>(); + doAnswer(invocation -> { + durableFingerprint.set(invocation.getArgument(0, SingleAlert.class).getFingerprint()); + latch.countDown(); + return null; + }).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class)); + SingleAlert alert = SingleAlert.builder() + .labels(new HashMap<>(Map.of( + "instance", "db-1", + "alertname", "disk_full", + "timestamp", "not-part-of-identity"))) + .build(); + + alarmCommonReduce.reduceAndSendAlarm(alert); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals("alertname:disk_full,instance:db-1", durableFingerprint.get()); + } + @Test void testReduceAndSendAlarmQueuesWhenConcurrencyLimitReached() throws Exception { VirtualThreadProperties properties = new VirtualThreadProperties(