This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 1b59bbd4b fix(alert): isolate each reconciliation state save in its
own transaction (#3039)
1b59bbd4b is described below
commit 1b59bbd4b6be94e42178a7a44fabaf2038b8bfc6
Author: cyberslack_lee <[email protected]>
AuthorDate: Tue Sep 15 19:30:45 2026 +0800
fix(alert): isolate each reconciliation state save in its own transaction
(#3039)
* Signed-off-by: enkilee <[email protected]>
fix Reconciliation 循环中单次 emitLifecycleEvent 失败回滚所有已保存的状态
* Signed-off-by: enkilee <[email protected]>
fix using comment suggestion
* Fix: fix code by comment
---
.../studio/ops/alert/NativeAlertProcessor.java | 37 +++++-
.../studio/ops/alert/NativeAlertProcessorTest.java | 146 +++++++++++++++++++--
2 files changed, 166 insertions(+), 17 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
index 2ae7b22d1..e4dacd3a8 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessor.java
@@ -23,7 +23,10 @@ import
org.apache.rocketmq.studio.cluster.metrics.MetricCollectionScope;
import org.apache.rocketmq.studio.cluster.metrics.MetricSample;
import org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
import org.springframework.stereotype.Component;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;
import java.time.Instant;
@@ -50,6 +53,7 @@ public class NativeAlertProcessor {
private final AlertRepository alertRepository;
private final NotificationOutboxService notificationOutboxService;
private final AlertNotificationSuppressionService
notificationSuppressionService;
+ private final PlatformTransactionManager transactionManager;
public void process(List<MetricSample> samples) {
processSamples(samples);
@@ -112,10 +116,14 @@ public class NativeAlertProcessor {
.map(rule -> new AlertStateKey(rule.getId(),
AlertFingerprint.of(rule.getId(),
sample.instanceId(), sample.labels()))))
.collect(Collectors.toSet());
- Map<Long, AlertRuleVO> byId =
rules.stream().collect(Collectors.toMap(AlertRuleVO::getId, rule -> rule));
+ Map<Long, AlertRuleVO> byId =
rules.stream().collect(Collectors.toMap(AlertRuleVO::getId, rule -> rule,
+ (left, right) -> left));
Instant resolvedAt =
samples.stream().filter(scope::contains).map(MetricSample::collectedAt).max(Instant::compareTo)
.orElseGet(Instant::now);
AlertEvaluationResult clear = new AlertEvaluationResult(true, false,
null, MetricAvailability.AVAILABLE);
+ TransactionTemplate isolatedTx = new
TransactionTemplate(transactionManager);
+ isolatedTx.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
+ int failedLifecycleEmits = 0;
for (ActiveAlertState active : stateRepository.findActive(scope,
rules)) {
if (presentKeys.contains(active.key())) {
continue;
@@ -130,11 +138,30 @@ public class NativeAlertProcessor {
if (update.transition() != AlertStateTransition.RESOLVED) {
continue;
}
- if (!stateRepository.save(active.key(), update.state())) {
- continue;
+ try {
+ // save + emit run inside a single REQUIRES_NEW
sub-transaction so that a
+ // transient emit failure rolls back both the state change and
the event,
+ // preventing orphaned states or events. The next collection
cycle will
+ // re-evaluate this active state and re-attempt the emit.
+ // save() may still return false inside the sub-transaction
when a
+ // concurrent writer (e.g. a user ACK) won the optimistic
race, so the
+ // emit is gated on the save result: an alert that already
moved on must
+ // not receive a RESOLVED event.
+ isolatedTx.executeWithoutResult(txStatus -> {
+ if (!stateRepository.save(active.key(), update.state())) {
+ return;
+ }
+ emitLifecycleEvent(rule, active.key(), update,
scope.domain(),
+ active.instanceId(), rule.getMetric(),
active.labels(), resolvedAt);
+ });
+ } catch (RuntimeException error) {
+ failedLifecycleEmits++;
+ log.warn("Native alert reconcile lifecycle emit failed:
ruleId={}, fingerprint={}, cause={}",
+ rule.getId(), active.key().fingerprint(),
error.getClass().getSimpleName());
}
- emitLifecycleEvent(rule, active.key(), update, scope.domain(),
active.instanceId(), rule.getMetric(),
- active.labels(), resolvedAt);
+ }
+ if (failedLifecycleEmits > 0) {
+ log.warn("Native alert reconcile completed with {} failed
lifecycle emit(s)", failedLifecycleEmits);
}
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
index 54c462407..552a78a34 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NativeAlertProcessorTest.java
@@ -21,6 +21,9 @@ import
org.apache.rocketmq.studio.cluster.metrics.MetricCollectionScope;
import org.apache.rocketmq.studio.cluster.metrics.MetricSample;
import org.apache.rocketmq.studio.cluster.metrics.MetricSnapshotRepository;
import org.junit.jupiter.api.Test;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
import java.time.Instant;
import java.util.HashMap;
@@ -33,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -103,7 +107,7 @@ class NativeAlertProcessorTest {
NativeAlertProcessor processor = new NativeAlertProcessor(service,
evaluationService,
new AlertStateMachine(), mock(AlertStateRepository.class),
mock(AlertRepository.class),
- mock(NotificationOutboxService.class), suppression());
+ mock(NotificationOutboxService.class), suppression(),
mockTxManager());
assertThatThrownBy(() -> processor.process(List.of(sample("orders"))))
.isInstanceOf(AssertionError.class)
@@ -354,7 +358,7 @@ class NativeAlertProcessorTest {
NativeAlertEvaluationService evaluationService = new
NativeAlertEvaluationService(new AlertRuleEvaluator(),
new AlertStateMachine(), states, snapshots, alerts, outbox,
suppression);
return new NativeAlertProcessor(service, evaluationService, new
AlertStateMachine(), states, alerts, outbox,
- suppression);
+ suppression, mockTxManager());
}
private static NativeAlertProcessor processor(AlertService service,
AlertStateRepository states,
@@ -431,11 +435,11 @@ class NativeAlertProcessorTest {
when(alerts.saveAlert(any(SystemAlertVO.class))).thenAnswer(invocation
-> invocation.getArgument(0));
NotificationOutboxService outbox =
mock(NotificationOutboxService.class);
- new NativeAlertProcessor(service,
+ NativeAlertProcessor processor = new NativeAlertProcessor(service,
new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
mock(MetricSnapshotRepository.class), alerts, outbox,
suppression()),
- new AlertStateMachine(), states, alerts, outbox, suppression())
- .processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
+ new AlertStateMachine(), states, alerts, outbox,
suppression(), mockTxManager());
+ processor.processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
java.util.Set.of("consumer.lag.total")), List.of());
org.mockito.ArgumentCaptor<AlertRuleState> state =
org.mockito.ArgumentCaptor.forClass(AlertRuleState.class);
@@ -466,12 +470,13 @@ class NativeAlertProcessorTest {
when(states.findActive(any(MetricCollectionScope.class),
eq(List.of(rule)))).thenReturn(List.of(active));
AlertRepository alerts = mock(AlertRepository.class);
- new NativeAlertProcessor(service,
+ NativeAlertProcessor processor = new NativeAlertProcessor(service,
new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
mock(MetricSnapshotRepository.class), alerts,
mock(NotificationOutboxService.class),
suppression()),
- new AlertStateMachine(), states, alerts,
mock(NotificationOutboxService.class), suppression())
- .processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
+ new AlertStateMachine(), states, alerts,
mock(NotificationOutboxService.class), suppression(),
+ mockTxManager());
+ processor.processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
java.util.Set.of("consumer.lag.total")),
List.of(current));
verify(alerts, never()).saveAlert(any(SystemAlertVO.class));
@@ -506,7 +511,7 @@ class NativeAlertProcessorTest {
new NativeAlertProcessor(service,
new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
mock(MetricSnapshotRepository.class), alerts, outbox,
suppression()),
- new AlertStateMachine(), states, alerts, outbox, suppression())
+ new AlertStateMachine(), states, alerts, outbox,
suppression(), mockTxManager())
.processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
java.util.Set.of("consumer.delay.seconds",
"consumer.lag.total")), List.of(lagSample));
@@ -536,12 +541,13 @@ class NativeAlertProcessorTest {
when(states.findActive(any(MetricCollectionScope.class),
eq(List.of(rule)))).thenReturn(List.of(active));
AlertRepository alerts = mock(AlertRepository.class);
- new NativeAlertProcessor(service,
+ NativeAlertProcessor processor = new NativeAlertProcessor(service,
new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
mock(MetricSnapshotRepository.class), alerts,
mock(NotificationOutboxService.class),
suppression()),
- new AlertStateMachine(), states, alerts,
mock(NotificationOutboxService.class), suppression())
- .processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
+ new AlertStateMachine(), states, alerts,
mock(NotificationOutboxService.class), suppression(),
+ mockTxManager());
+ processor.processSuccessfulCollection(new
MetricCollectionScope(AlertDomain.BUSINESS, "local",
java.util.Set.of("consumer.lag.total")), List.of(new
MetricSample("consumer.lag.total",
AlertDomain.BUSINESS, "local", null, Map.of(), null,
MetricAvailability.UNAVAILABLE,
Instant.now(), "BUSINESS_METRICS_COLLECTION_FAILED")));
@@ -550,6 +556,122 @@ class NativeAlertProcessorTest {
verify(alerts, never()).saveAlert(any(SystemAlertVO.class));
}
+ @Test
+ void singleLifecycleEmitFailureDoesNotRollBackTheBatchTest() {
+ AlertService service = mock(AlertService.class);
+ AlertRuleVO rule1 = rule(1L, "local", "orders", 1);
+ AlertRuleVO rule2 = rule(2L, "local", "payments", 1);
+
when(service.listRules(AlertDomain.BUSINESS)).thenReturn(List.of(rule1, rule2));
+ AlertStateRepository states = mock(AlertStateRepository.class);
+ when(states.save(any(AlertStateKey.class),
any(AlertRuleState.class))).thenReturn(true);
+ MetricSample ordersSample = sample("orders");
+ MetricSample paymentsSample = sample("payments");
+ ActiveAlertState active1 = new ActiveAlertState(
+ new AlertStateKey(1L, AlertFingerprint.of(1L, "local",
ordersSample.labels())),
+ new AlertRuleState(AlertStateStatus.FIRING, 1, 20D,
Instant.now().minusSeconds(60),
+ Instant.now().minusSeconds(60),
Instant.now().minusSeconds(60), null),
+ "local", ordersSample.labels());
+ ActiveAlertState active2 = new ActiveAlertState(
+ new AlertStateKey(2L, AlertFingerprint.of(2L, "local",
paymentsSample.labels())),
+ new AlertRuleState(AlertStateStatus.FIRING, 1, 20D,
Instant.now().minusSeconds(60),
+ Instant.now().minusSeconds(60),
Instant.now().minusSeconds(60), null),
+ "local", paymentsSample.labels());
+ when(states.findActive(any(MetricCollectionScope.class),
any())).thenReturn(List.of(active1, active2));
+ AlertRepository alerts = mock(AlertRepository.class);
+ when(alerts.saveAlert(any(SystemAlertVO.class)))
+ .thenThrow(new IllegalStateException("db write failed"))
+ .thenAnswer(invocation -> invocation.getArgument(0));
+ NotificationOutboxService outbox =
mock(NotificationOutboxService.class);
+
+ NativeAlertProcessor processor = new NativeAlertProcessor(service,
+ new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
+ mock(MetricSnapshotRepository.class), alerts, outbox,
suppression()),
+ new AlertStateMachine(), states, alerts, outbox,
suppression(), mockTxManager());
+ assertThatCode(() -> processor.processSuccessfulCollection(
+ new MetricCollectionScope(AlertDomain.BUSINESS, "local",
+ java.util.Set.of("consumer.lag.total")),
+ List.of())).doesNotThrowAnyException();
+
+ verify(outbox).enqueue(any(SystemAlertVO.class), eq(rule2), anyMap());
+ }
+
+ @Test
+ void emitFailureRollsBackBothSaveAndEmitPreventingOrphanEventsTest() {
+ AlertService service = mock(AlertService.class);
+ AlertRuleVO rule = rule("local", "orders", 1);
+
when(service.listRules(AlertDomain.BUSINESS)).thenReturn(List.of(rule));
+ MetricSample oldSample = sample("orders");
+ AlertStateKey oldKey = new AlertStateKey(rule.getId(),
+ AlertFingerprint.of(rule.getId(), oldSample.instanceId(),
oldSample.labels()));
+ ActiveAlertState active = new ActiveAlertState(oldKey,
+ new AlertRuleState(AlertStateStatus.FIRING, 1, 20D,
oldSample.collectedAt().minusSeconds(60),
+ oldSample.collectedAt().minusSeconds(60),
oldSample.collectedAt().minusSeconds(60), null),
+ oldSample.instanceId(), oldSample.labels());
+ AlertStateRepository states = mock(AlertStateRepository.class);
+ when(states.findActive(any(MetricCollectionScope.class),
eq(List.of(rule)))).thenReturn(List.of(active));
+ when(states.save(eq(oldKey),
any(AlertRuleState.class))).thenReturn(true);
+ AlertRepository alerts = mock(AlertRepository.class);
+ when(alerts.saveAlert(any(SystemAlertVO.class)))
+ .thenThrow(new IllegalStateException("event persist failed"));
+ NotificationOutboxService outbox =
mock(NotificationOutboxService.class);
+ PlatformTransactionManager txManager = mockTxManager();
+
+ NativeAlertProcessor processor = new NativeAlertProcessor(service,
+ new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
+ mock(MetricSnapshotRepository.class), alerts, outbox,
suppression()),
+ new AlertStateMachine(), states, alerts, outbox,
suppression(), txManager);
+ assertThatCode(() -> processor.processSuccessfulCollection(
+ new MetricCollectionScope(AlertDomain.BUSINESS, "local",
+ java.util.Set.of("consumer.lag.total")),
+ List.of())).doesNotThrowAnyException();
+
+ verify(states).save(eq(oldKey), any(AlertRuleState.class));
+ verify(alerts).saveAlert(any(SystemAlertVO.class));
+ verify(outbox, never()).enqueue(any(), any(), any());
+ verify(txManager).rollback(any(TransactionStatus.class));
+ }
+
+ @Test
+ void doesNotEmitResolvedEventWhenStateSaveLosesTheOptimisticRaceTest() {
+ AlertService service = mock(AlertService.class);
+ AlertRuleVO rule = rule("local", "orders", 1);
+
when(service.listRules(AlertDomain.BUSINESS)).thenReturn(List.of(rule));
+ MetricSample oldSample = sample("orders");
+ AlertStateKey oldKey = new AlertStateKey(rule.getId(),
+ AlertFingerprint.of(rule.getId(), oldSample.instanceId(),
oldSample.labels()));
+ ActiveAlertState active = new ActiveAlertState(oldKey,
+ new AlertRuleState(AlertStateStatus.FIRING, 1, 20D,
oldSample.collectedAt().minusSeconds(60),
+ oldSample.collectedAt().minusSeconds(60),
oldSample.collectedAt().minusSeconds(60), null),
+ oldSample.instanceId(), oldSample.labels());
+ AlertStateRepository states = mock(AlertStateRepository.class);
+ when(states.findActive(any(MetricCollectionScope.class),
eq(List.of(rule)))).thenReturn(List.of(active));
+ // a concurrent ACK already advanced the state, so this writer's save
loses the
+ // optimistic race and returns false
+ when(states.save(eq(oldKey),
any(AlertRuleState.class))).thenReturn(false);
+ AlertRepository alerts = mock(AlertRepository.class);
+ NotificationOutboxService outbox =
mock(NotificationOutboxService.class);
+
+ NativeAlertProcessor processor = new NativeAlertProcessor(service,
+ new NativeAlertEvaluationService(new AlertRuleEvaluator(), new
AlertStateMachine(), states,
+ mock(MetricSnapshotRepository.class), alerts, outbox,
suppression()),
+ new AlertStateMachine(), states, alerts, outbox,
suppression(), mockTxManager());
+ assertThatCode(() -> processor.processSuccessfulCollection(
+ new MetricCollectionScope(AlertDomain.BUSINESS, "local",
+ java.util.Set.of("consumer.lag.total")),
+ List.of())).doesNotThrowAnyException();
+
+ verify(states).save(eq(oldKey), any(AlertRuleState.class));
+ verify(alerts, never()).saveAlert(any(SystemAlertVO.class));
+ verify(outbox, never()).enqueue(any(), any(), any());
+ }
+
+ private static PlatformTransactionManager mockTxManager() {
+ PlatformTransactionManager txManager =
mock(PlatformTransactionManager.class);
+ TransactionStatus status = mock(TransactionStatus.class);
+
when(txManager.getTransaction(any(TransactionDefinition.class))).thenReturn(status);
+ return txManager;
+ }
+
private static AlertNotificationSuppressionService suppression() {
AlertNotificationSuppressionService service =
mock(AlertNotificationSuppressionService.class);
when(service.findSuppressingClusterAlert(any(SystemAlertVO.class))).thenReturn(Optional.empty());