From f2e1a74cbca8da13b634426c579e9f3085463812 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Thu, 16 Apr 2026 12:12:20 +0200 Subject: [PATCH 1/2] fix: resolve REINIT crash when switching alarm duration from static to dynamic Move fetchArguments() and state.update() before state.init() in initState() so that arguments are available when init() triggers reeval for DURATION conditions with active tracking (firstEventTs > 0). --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- .../thingsboard/server/cf/AlarmRulesTest.java | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index a2dc9b50f4..b69719f1a0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -485,7 +485,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) { state.setCtx(ctx, actorCtx); - state.init(false); if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isCfHasRelationPathQuerySource()) { GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state; @@ -494,6 +493,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM Map arguments = fetchArguments(ctx); state.update(arguments, ctx); + state.init(false); state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); states.put(ctx.getCfId(), state); diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index cf94940e07..3a1446fb46 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -402,6 +402,54 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testChangeDurationConditionFromStaticToDynamic() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = new HashMap<>(Map.of( + "temperature", temperatureArgument + )); + + long staticDurationMs = 5000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, staticDurationMs) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + // post telemetry to trigger condition, so that firstEventTs > 0 in AlarmRuleState + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + + // update CF: add attribute argument and switch duration from static to dynamic + AlarmCalculatedFieldConfiguration configuration = + (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + + Argument durationArgument = new Argument(); + durationArgument.setRefEntityKey(new ReferencedEntityKey("durationThreshold", + ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + durationArgument.setDefaultValue("-1"); + configuration.getArguments().put("durationThreshold", durationArgument); + + DurationAlarmCondition durationCondition = (DurationAlarmCondition) + configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition(); + durationCondition.setValue(new AlarmConditionValue<>(null, "durationThreshold")); + + calculatedField = saveCalculatedField(calculatedField); + + long dynamicDurationMs = 3000L; + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, + "{\"durationThreshold\":" + dynamicDurationMs + "}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + @Test public void testCreateAlarm_currentOwnerArgument() throws Exception { Argument temperatureArgument = new Argument(); From 7131e4018d9204499c552192b162d9399f02f986 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Mon, 4 May 2026 10:21:40 +0200 Subject: [PATCH 2/2] test: replace Thread.sleep with await on debug event before REINIT Guarantees firstEventTs > 0 in AlarmRuleState before saveCalculatedField triggers REINIT, so the test reliably exercises the buggy reeval path on slow CI; otherwise ruleState.isEmpty() may stay true and the alarm gets created via the fallback path even without the fix. --- .../org/thingsboard/server/cf/AlarmRulesTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index 3a1446fb46..e13872e46e 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -407,9 +407,9 @@ public class AlarmRulesTest extends AbstractControllerTest { Argument temperatureArgument = new Argument(); temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); temperatureArgument.setDefaultValue("0"); - Map arguments = new HashMap<>(Map.of( + Map arguments = Map.of( "temperature", temperatureArgument - )); + ); long staticDurationMs = 5000L; Map createRules = Map.of( @@ -419,9 +419,13 @@ public class AlarmRulesTest extends AbstractControllerTest { CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", arguments, createRules, null); - // post telemetry to trigger condition, so that firstEventTs > 0 in AlarmRuleState + // post telemetry to trigger condition and wait for the static-phase eval to produce a debug event, + // which guarantees firstEventTs > 0 in AlarmRuleState before we trigger REINIT postTelemetry(deviceId, "{\"temperature\":50}"); - Thread.sleep(1000); + CalculatedFieldId cfId = calculatedField.getId(); + await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getDebugEvents(cfId, 1), + events -> !events.isEmpty() && !events.get(0).getId().equals(latestEventId)); // update CF: add attribute argument and switch duration from static to dynamic AlarmCalculatedFieldConfiguration configuration =