From 78c4892ad4d0c99ce70315821080deaf5bbd7c1a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 22 Oct 2025 13:17:41 +0300 Subject: [PATCH 1/8] Added Readiness Status for CF state --- ...CalculatedFieldEntityMessageProcessor.java | 6 +++- .../ctx/state/BaseCalculatedFieldState.java | 18 +++++++++-- .../cf/ctx/state/CalculatedFieldState.java | 31 ++++++++++++++++++- .../ctx/state/SingleValueArgumentEntry.java | 3 ++ .../propagation/PropagationArgumentEntry.java | 3 +- .../PropagationCalculatedFieldState.java | 13 ++------ .../GeofencingCalculatedFieldStateTest.java | 6 ++-- .../state/PropagationArgumentEntryTest.java | 16 ---------- .../PropagationCalculatedFieldStateTest.java | 10 +++--- .../state/ScriptCalculatedFieldStateTest.java | 6 ++-- .../state/SimpleCalculatedFieldStateTest.java | 6 ++-- 11 files changed, 72 insertions(+), 46 deletions(-) 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 033292c23e..a7fb74f432 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 @@ -399,7 +399,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeChecked = false; try { - if (ctx.isInitialized() && state.isReady()) { + if (ctx.isInitialized() && state.getReadinessStatus().status()) { log.trace("[{}][{}] Performing calculation. Updated args: {}", entityId, ctx.getCfId(), updatedArgs); CalculatedFieldResult calculationResult = state.performCalculation(updatedArgs, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); @@ -415,6 +415,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } } else { + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().reason() : "Calculated field state is not initialized!"; + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); + } callback.onSuccess(); } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index a3966d8a73..8a1b7e64e6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -26,6 +26,7 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; import java.io.Closeable; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -107,9 +108,20 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, } @Override - public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + public ReadinessStatus getReadinessStatus() { + List missing = new ArrayList<>(requiredArguments); + missing.removeAll(arguments.keySet()); + if (!missing.isEmpty()) { + return ReadinessStatus.missingRequiredArguments(missing); + } + List emptyArgs = arguments.entrySet().stream() + .filter(e -> e.getValue() == null || e.getValue().isEmpty()) + .map(Map.Entry::getKey) + .toList(); + if (!emptyArgs.isEmpty()) { + return ReadinessStatus.emptyArguments(emptyArgs); + } + return ReadinessStatus.ready(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 28a14c921a..2bfe09b813 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.Nullable; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.EntityId; @@ -32,6 +33,7 @@ import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculat import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.io.Closeable; +import java.util.List; import java.util.Map; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; @@ -66,7 +68,7 @@ public interface CalculatedFieldState extends Closeable { ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx); @JsonIgnore - boolean isReady(); + ReadinessStatus getReadinessStatus(); boolean isSizeExceedsLimit(); @@ -92,4 +94,31 @@ public interface CalculatedFieldState extends Closeable { } } + record ReadinessStatus(boolean status, @Nullable String reason) { + + private static final String MISSING_REQUIRED_ARGUMENTS = "Missing required arguments: "; + private static final String EMPTY_ARGUMENTS = "Empty arguments: "; + + public static ReadinessStatus ready() { + return new ReadinessStatus(true, null); + } + + public static ReadinessStatus notReady(String reason) { + return new ReadinessStatus(false, reason); + } + + public static ReadinessStatus missingRequiredArguments(List missingArgument) { + return notReady(MISSING_REQUIRED_ARGUMENTS + stringValue(missingArgument)); + } + + private static String stringValue(List missingArgument) { + return String.join(", ", missingArgument); + } + + public static ReadinessStatus emptyArguments(List emptyArguments) { + return notReady(EMPTY_ARGUMENTS + stringValue(emptyArguments)); + } + + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 5c1ed32e1d..9fd2ad1662 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -95,6 +95,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { @Override public TbelCfArg toTbelCfArg() { + if (isEmpty()) { + return new TbelCfSingleValueArg(ts, null); + } Object value = kvEntryValue.getValue(); if (kvEntryValue instanceof JsonDataEntry) { try { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index c7d49a4d40..81009da5e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import java.util.ArrayList; import java.util.List; @Data @@ -33,7 +34,7 @@ public class PropagationArgumentEntry implements ArgumentEntry { private boolean forceResetPrevious; public PropagationArgumentEntry(List propagationEntityIds) { - this.propagationEntityIds = propagationEntityIds; + this.propagationEntityIds = new ArrayList<>(propagationEntityIds); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 01e9a73de8..cc22797593 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -33,6 +33,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import java.util.ArrayList; import java.util.Map; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; @@ -47,21 +48,13 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { this.ctx = ctx; this.actorCtx = actorCtx; - this.requiredArguments = ctx.getArgNames(); + this.requiredArguments = new ArrayList<>(ctx.getArgNames()); + requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); if (ctx.isApplyExpressionForResolvedArguments()) { this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } } - @Override - public boolean isReady() { - if (!super.isReady()) { - return false; - } - ArgumentEntry propagationArg = arguments.get(PROPAGATION_CONFIG_ARGUMENT); - return propagationArg != null && !propagationArg.isEmpty(); - } - @Override public CalculatedFieldType getType() { return CalculatedFieldType.PROPAGATION; diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 5ca68d4e1b..fd41d633ef 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -199,7 +199,7 @@ public class GeofencingCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test @@ -210,7 +210,7 @@ public class GeofencingCalculatedFieldStateTest { "allowedZones", geofencingAllowedZoneArgEntry, "restrictedZones", geofencingRestrictedZoneArgEntry )); - assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().status()).isTrue(); } @Test @@ -224,7 +224,7 @@ public class GeofencingCalculatedFieldStateTest { state.getArguments().put("noParkingZones", new GeofencingArgumentEntry()); - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java index 9c8a788e15..14a1b629c1 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -57,12 +57,6 @@ public class PropagationArgumentEntryTest { assertThat(emptyEntry.isEmpty()).isTrue(); } - @Test - void testIsEmptyWhenNullList() { - PropagationArgumentEntry nullListEntry = new PropagationArgumentEntry(null); - assertThat(nullListEntry.isEmpty()).isTrue(); - } - @Test void testGetValueReturnsPropagationIds() { assertThat(entry.getValue()).isInstanceOf(List.class); @@ -106,16 +100,6 @@ public class PropagationArgumentEntryTest { assertThat(entry.getPropagationEntityIds()).isEmpty(); } - @Test - void testUpdateEntryClearsWhenNewEntryIsNullList() { - var updatedNull = new PropagationArgumentEntry(null); - - boolean changed = entry.updateEntry(updatedNull); - - assertThat(changed).isTrue(); - assertThat(entry.getPropagationEntityIds()).isEmpty(); - } - @Test @SuppressWarnings("unchecked") void testToTbelCfArgWithValues() { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index 04a7ab5203..ac9fdefff6 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -116,20 +116,20 @@ public class PropagationCalculatedFieldStateTest { @Test void testInitAddsRequiredArgument() { initCtxAndState(false); - assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME); + assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); } @Test void testIsReadyReturnFalseWhenNoArgumentsSet() { initCtxAndState(false); - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test void testIsReadyWhenPropagationArgIsNull() { initCtxAndState(false); state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test @@ -137,7 +137,7 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test @@ -145,7 +145,7 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().status()).isTrue(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index e46f3e1c15..8db34a884f 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -160,21 +160,21 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test void testIsReadyWhenAllArgPresent() { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().status()).isTrue(); } @Test void testIsReadyWhenEmptyEntryPresents() { state.arguments = new HashMap<>(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry)); - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } private TsRollingArgumentEntry createRollingArgEntry() { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index df8bf1fbba..6af253ff1b 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -203,7 +203,7 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } @Test @@ -214,7 +214,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().status()).isTrue(); } @Test @@ -225,7 +225,7 @@ public class SimpleCalculatedFieldStateTest { )); state.getArguments().put("key3", new SingleValueArgumentEntry()); - assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().status()).isFalse(); } private CalculatedField getCalculatedField() { From aaab42c86a1bb21f9b9ab90a6be5bec2c49a8429 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 27 Oct 2025 10:39:05 +0200 Subject: [PATCH 2/8] import/export new cfs & Readiness status updates dues to comments --- ...CalculatedFieldEntityMessageProcessor.java | 4 +- .../ctx/state/BaseCalculatedFieldState.java | 26 +++----- .../cf/ctx/state/CalculatedFieldState.java | 62 ++++++++++++++----- .../PropagationCalculatedFieldState.java | 1 + .../impl/DefaultEntityExportService.java | 21 +++++-- .../impl/BaseEntityImportService.java | 21 +++++-- .../GeofencingCalculatedFieldStateTest.java | 26 ++++---- .../PropagationCalculatedFieldStateTest.java | 22 ++++--- .../state/ScriptCalculatedFieldStateTest.java | 18 +++--- .../state/SimpleCalculatedFieldStateTest.java | 27 ++++---- 10 files changed, 141 insertions(+), 87 deletions(-) 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 55d59ba50d..aef81474ec 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 @@ -399,7 +399,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeChecked = false; try { - if (ctx.isInitialized() && state.getReadinessStatus().status()) { + if (ctx.isInitialized() && state.isReady()) { log.trace("[{}][{}] Performing calculation. Updated args: {}", entityId, ctx.getCfId(), updatedArgs); CalculatedFieldResult calculationResult = state.performCalculation(updatedArgs, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); @@ -416,7 +416,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } else { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { - String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().reason() : "Calculated field state is not initialized!"; + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().stringValue() : "Calculated field state is not initialized!"; systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 8a1b7e64e6..7272629f80 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -26,11 +26,11 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; import java.io.Closeable; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; @Getter public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { @@ -43,6 +43,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, protected Map arguments = new HashMap<>(); protected boolean sizeExceedsLimit; protected long latestTimestamp = -1; + protected ReadinessStatus readinessStatus; @Setter private TopicPartitionInfo partition; @@ -56,6 +57,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, this.ctx = ctx; this.actorCtx = actorCtx; this.requiredArguments = ctx.getArgNames(); + this.readinessStatus = ReadinessStatus.initialState(requiredArguments, arguments); } @Override @@ -89,14 +91,12 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, } updatedArguments.put(key, newEntry); updateLastUpdateTimestamp(newEntry); + readinessStatus.onArgumentUpdate(key, newEntry); } } - if (updatedArguments == null) { - updatedArguments = Collections.emptyMap(); - } - return updatedArguments; + return Objects.requireNonNullElse(updatedArguments, Collections.emptyMap()); } @Override @@ -108,20 +108,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, } @Override - public ReadinessStatus getReadinessStatus() { - List missing = new ArrayList<>(requiredArguments); - missing.removeAll(arguments.keySet()); - if (!missing.isEmpty()) { - return ReadinessStatus.missingRequiredArguments(missing); - } - List emptyArgs = arguments.entrySet().stream() - .filter(e -> e.getValue() == null || e.getValue().isEmpty()) - .map(Map.Entry::getKey) - .toList(); - if (!emptyArgs.isEmpty()) { - return ReadinessStatus.emptyArguments(emptyArgs); - } - return ReadinessStatus.ready(); + public boolean isReady() { + return readinessStatus.isReady(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 2bfe09b813..28fa70e77c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -16,11 +16,14 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; -import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.EntityId; @@ -33,8 +36,10 @@ import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculat import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.io.Closeable; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; @@ -68,6 +73,8 @@ public interface CalculatedFieldState extends Closeable { ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx); @JsonIgnore + boolean isReady(); + ReadinessStatus getReadinessStatus(); boolean isSizeExceedsLimit(); @@ -94,29 +101,52 @@ public interface CalculatedFieldState extends Closeable { } } - record ReadinessStatus(boolean status, @Nullable String reason) { - - private static final String MISSING_REQUIRED_ARGUMENTS = "Missing required arguments: "; - private static final String EMPTY_ARGUMENTS = "Empty arguments: "; + @Data + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class ReadinessStatus { - public static ReadinessStatus ready() { - return new ReadinessStatus(true, null); - } + private Set missingArguments; + private Set emptyArguments; - public static ReadinessStatus notReady(String reason) { - return new ReadinessStatus(false, reason); + public static ReadinessStatus initialState(List requiredArguments, Map arguments) { + if (arguments.isEmpty()) { + return new ReadinessStatus(new HashSet<>(requiredArguments), new HashSet<>()); + } + Set missingArguments = new HashSet<>(requiredArguments.size()); + Set emptyArguments = new HashSet<>(requiredArguments.size()); + requiredArguments.forEach(requiredArgumentKey -> { + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); + if (argumentEntry == null) { + missingArguments.add(requiredArgumentKey); + return; + } + if (argumentEntry.isEmpty()) { + emptyArguments.add(requiredArgumentKey); + } + }); + return new ReadinessStatus(missingArguments, emptyArguments); } - public static ReadinessStatus missingRequiredArguments(List missingArgument) { - return notReady(MISSING_REQUIRED_ARGUMENTS + stringValue(missingArgument)); + public void onArgumentUpdate(String key, ArgumentEntry newEntry) { + if (newEntry == null) { + missingArguments.add(key); + return; + } + missingArguments.remove(key); + if (newEntry.isEmpty()) { + emptyArguments.add(key); + } else { + emptyArguments.remove(key); + } } - private static String stringValue(List missingArgument) { - return String.join(", ", missingArgument); + public boolean isReady() { + return missingArguments.isEmpty() && emptyArguments.isEmpty(); } - public static ReadinessStatus emptyArguments(List emptyArguments) { - return notReady(EMPTY_ARGUMENTS + stringValue(emptyArguments)); + public String stringValue() { + return JacksonUtil.toString(this); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index cc22797593..db708b51af 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -50,6 +50,7 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState this.actorCtx = actorCtx; this.requiredArguments = new ArrayList<>(ctx.getArgNames()); requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); + this.readinessStatus = ReadinessStatus.initialState(requiredArguments, arguments); if (ctx.isApplyExpressionForResolvedArguments()) { this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index b7b027d785..5d634c4178 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -154,12 +155,20 @@ public class DefaultEntityExportService calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId); calculatedFields.forEach(calculatedField -> { calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId)); - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration) { - configuration.getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); - } - }); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(getExternalIdOrElseInternal(ctx, zoneGroupConfiguration.getRefEntityId())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + } + }); + } } }); return calculatedFields; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 2b606a2fe8..c95fffc205 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -322,12 +323,20 @@ public abstract class BaseEntityImportService { calculatedField.setTenantId(ctx.getTenantId()); calculatedField.setEntityId(savedEntity.getId()); - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration) { - configuration.getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); - } - }); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(idProvider.getInternalId(zoneGroupConfiguration.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } } }).toList(); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index fd41d633ef..5e304e7442 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -199,32 +199,36 @@ public class GeofencingCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { - assertThat(state.getReadinessStatus().status()).isFalse(); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getMissingArguments()) + .containsExactlyInAnyOrderElementsOf(state.requiredArguments); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, "allowedZones", geofencingAllowedZoneArgEntry, "restrictedZones", geofencingRestrictedZoneArgEntry - )); - assertThat(state.getReadinessStatus().status()).isTrue(); + ), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, "allowedZones", geofencingAllowedZoneArgEntry, - "restrictedZones", geofencingRestrictedZoneArgEntry - )); - - state.getArguments().put("noParkingZones", new GeofencingArgumentEntry()); - - assertThat(state.getReadinessStatus().status()).isFalse(); + "restrictedZones", new GeofencingArgumentEntry() + ), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).contains("restrictedZones"); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index 743e0194bf..11ec6031a5 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -123,30 +123,34 @@ public class PropagationCalculatedFieldStateTest { @Test void testIsReadyReturnFalseWhenNoArgumentsSet() { initCtxAndState(false); - assertThat(state.getReadinessStatus().status()).isFalse(); + assertThat(state.isReady()).isFalse(); } @Test void testIsReadyWhenPropagationArgIsNull() { initCtxAndState(false); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - assertThat(state.getReadinessStatus().status()).isFalse(); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getMissingArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); } @Test void testIsReadyWhenPropagationArgIsEmpty() { initCtxAndState(false); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); - assertThat(state.getReadinessStatus().status()).isFalse(); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.isReady()).isFalse(); } @Test void testIsReadyWhenPropagationArgHasEntities() { initCtxAndState(false); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - assertThat(state.getReadinessStatus().status()).isTrue(); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 8db34a884f..283b321587 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -160,21 +160,25 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { - assertThat(state.getReadinessStatus().status()).isFalse(); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getMissingArguments()) + .containsExactlyInAnyOrderElementsOf(state.requiredArguments); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - - assertThat(state.getReadinessStatus().status()).isTrue(); + state.update(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry)); - - assertThat(state.getReadinessStatus().status()).isFalse(); + state.update(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly("deviceTemperature"); } private TsRollingArgumentEntry createRollingArgEntry() { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 6af253ff1b..69674dc370 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -203,29 +203,34 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { - assertThat(state.getReadinessStatus().status()).isFalse(); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getMissingArguments()) + .containsExactlyInAnyOrderElementsOf(state.requiredArguments); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, "key2", key2ArgEntry, "key3", key3ArgEntry - )); - - assertThat(state.getReadinessStatus().status()).isTrue(); + ), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, - "key2", key2ArgEntry - )); - state.getArguments().put("key3", new SingleValueArgumentEntry()); - - assertThat(state.getReadinessStatus().status()).isFalse(); + "key2", key2ArgEntry, + "key3", new SingleValueArgumentEntry() + ), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly("key3"); } private CalculatedField getCalculatedField() { From b8f4d986b923a4cd1e0088ff3949ee11732c110b Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 27 Oct 2025 14:39:22 +0200 Subject: [PATCH 3/8] Fixed CalculatedFieldUtilsTest --- .../utils/CalculatedFieldUtilsTest.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 505837ece2..64b2fb032e 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -47,6 +47,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @@ -91,14 +92,22 @@ class CalculatedFieldUtilsTest { // Create cf state with the geofencing argument and add it to the state map CalculatedFieldState state = new GeofencingCalculatedFieldState(DEVICE_ID); - state.update(Map.of("geofencingArgumentTest", geofencingArgumentEntry), mock(CalculatedFieldCtx.class)); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("geofencingArgumentTest")); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of("geofencingArgumentTest", geofencingArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(1); + assertThat(updatedArguments.get("geofencingArgumentTest")).isEqualTo(geofencingArgumentEntry); CalculatedFieldStateProto proto = toProto(stateId, state); CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(stateId, proto); assertThat(fromProto) .usingRecursiveComparison() - .ignoringFields("requiredArguments") + .ignoringFields("ctx", "requiredArguments", "readinessStatus") .isEqualTo(state); ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest"); @@ -124,9 +133,16 @@ class CalculatedFieldUtilsTest { SingleValueArgumentEntry singleValueArgumentEntry = new SingleValueArgumentEntry(new BaseAttributeKvEntry(new StringDataEntry("state", "active"), lastUpdateTs, 1L)); CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("state")); CalculatedFieldState state = new PropagationCalculatedFieldState(DEVICE_ID); - state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry, "state", singleValueArgumentEntry), cfCtxMock); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry, "state", singleValueArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(2); + assertThat(updatedArguments.get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); + assertThat(updatedArguments.get("state")).isEqualTo(singleValueArgumentEntry); // when CalculatedFieldStateProto proto = toProto(stateId, state); @@ -144,6 +160,8 @@ class CalculatedFieldUtilsTest { assertThat(propagationState.getArguments()).isNotNull(); assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isNull(); assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); + assertThat(propagationState.getRequiredArguments()).isNull(); + assertThat(propagationState.getReadinessStatus()).isNull(); } } From de514ff4d02ac64dfefa0011785e164053e4ac26 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 27 Oct 2025 17:35:36 +0200 Subject: [PATCH 4/8] simplified logic for readinessStatus --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- .../ctx/state/BaseCalculatedFieldState.java | 26 +++++++++--- .../cf/ctx/state/CalculatedFieldState.java | 42 ++----------------- .../PropagationCalculatedFieldState.java | 1 - .../GeofencingCalculatedFieldStateTest.java | 8 +--- .../PropagationCalculatedFieldStateTest.java | 8 ++-- .../state/ScriptCalculatedFieldStateTest.java | 7 +--- .../state/SimpleCalculatedFieldStateTest.java | 8 +--- 8 files changed, 35 insertions(+), 67 deletions(-) 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 aef81474ec..fb0f92c2ab 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 @@ -416,7 +416,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } else { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { - String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().stringValue() : "Calculated field state is not initialized!"; + String errorMsg = ctx.isInitialized() && state.getReadinessStatus() != null ? state.getReadinessStatus().stringValue() : "Calculated field state is not initialized!"; systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 7272629f80..ac9a6a929b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -26,11 +26,11 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; import java.io.Closeable; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; @Getter public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { @@ -57,7 +57,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, this.ctx = ctx; this.actorCtx = actorCtx; this.requiredArguments = ctx.getArgNames(); - this.readinessStatus = ReadinessStatus.initialState(requiredArguments, arguments); } @Override @@ -91,12 +90,15 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, } updatedArguments.put(key, newEntry); updateLastUpdateTimestamp(newEntry); - readinessStatus.onArgumentUpdate(key, newEntry); } } - return Objects.requireNonNullElse(updatedArguments, Collections.emptyMap()); + if (updatedArguments != null) { + readinessStatus = checkReadiness(requiredArguments, arguments); + return updatedArguments; + } + return Collections.emptyMap(); } @Override @@ -109,7 +111,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, @Override public boolean isReady() { - return readinessStatus.isReady(); + return readinessStatus != null && readinessStatus.isReady(); } @Override @@ -150,4 +152,18 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } + public ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } + } + return new ReadinessStatus(emptyArguments); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 28fa70e77c..ef3139396a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -36,10 +36,8 @@ import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculat import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.io.Closeable; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; @@ -103,52 +101,20 @@ public interface CalculatedFieldState extends Closeable { @Data @AllArgsConstructor - @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonInclude(JsonInclude.Include.NON_NULL) class ReadinessStatus { - private Set missingArguments; - private Set emptyArguments; - - public static ReadinessStatus initialState(List requiredArguments, Map arguments) { - if (arguments.isEmpty()) { - return new ReadinessStatus(new HashSet<>(requiredArguments), new HashSet<>()); - } - Set missingArguments = new HashSet<>(requiredArguments.size()); - Set emptyArguments = new HashSet<>(requiredArguments.size()); - requiredArguments.forEach(requiredArgumentKey -> { - ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); - if (argumentEntry == null) { - missingArguments.add(requiredArgumentKey); - return; - } - if (argumentEntry.isEmpty()) { - emptyArguments.add(requiredArgumentKey); - } - }); - return new ReadinessStatus(missingArguments, emptyArguments); - } - - public void onArgumentUpdate(String key, ArgumentEntry newEntry) { - if (newEntry == null) { - missingArguments.add(key); - return; - } - missingArguments.remove(key); - if (newEntry.isEmpty()) { - emptyArguments.add(key); - } else { - emptyArguments.remove(key); - } - } + private List emptyArguments; public boolean isReady() { - return missingArguments.isEmpty() && emptyArguments.isEmpty(); + return emptyArguments == null || emptyArguments.isEmpty(); } public String stringValue() { return JacksonUtil.toString(this); } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index db708b51af..cc22797593 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -50,7 +50,6 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState this.actorCtx = actorCtx; this.requiredArguments = new ArrayList<>(ctx.getArgNames()); requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); - this.readinessStatus = ReadinessStatus.initialState(requiredArguments, arguments); if (ctx.isApplyExpressionForResolvedArguments()) { this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 5e304e7442..a91834a798 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -200,9 +200,7 @@ public class GeofencingCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getMissingArguments()) - .containsExactlyInAnyOrderElementsOf(state.requiredArguments); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus()).isNull(); } @Test @@ -214,8 +212,7 @@ public class GeofencingCalculatedFieldStateTest { "restrictedZones", geofencingRestrictedZoneArgEntry ), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); } @Test @@ -227,7 +224,6 @@ public class GeofencingCalculatedFieldStateTest { "restrictedZones", new GeofencingArgumentEntry() ), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); assertThat(state.getReadinessStatus().getEmptyArguments()).contains("restrictedZones"); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index 11ec6031a5..a03be03f59 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -131,7 +131,7 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getMissingArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); } @Test @@ -139,9 +139,8 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); } @Test @@ -149,8 +148,7 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 283b321587..a8dcc18025 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -161,17 +161,14 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getMissingArguments()) - .containsExactlyInAnyOrderElementsOf(state.requiredArguments); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus()).isNull(); } @Test void testIsReadyWhenAllArgPresent() { state.update(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 69674dc370..7748186919 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -204,9 +204,7 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getMissingArguments()) - .containsExactlyInAnyOrderElementsOf(state.requiredArguments); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus()).isNull(); } @Test @@ -217,8 +215,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry ), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isEmpty(); + assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); } @Test @@ -229,7 +226,6 @@ public class SimpleCalculatedFieldStateTest { "key3", new SingleValueArgumentEntry() ), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getMissingArguments()).isEmpty(); assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly("key3"); } From f5fc499662bc2fd45b75d4f70b51e9e0c7761ccb Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 27 Oct 2025 18:01:07 +0200 Subject: [PATCH 5/8] rollback set of readinessStatus on setCtx --- .../cf/ctx/state/BaseCalculatedFieldState.java | 12 ++++++++---- .../propagation/PropagationCalculatedFieldState.java | 1 + .../state/GeofencingCalculatedFieldStateTest.java | 2 +- .../cf/ctx/state/ScriptCalculatedFieldStateTest.java | 2 +- .../cf/ctx/state/SimpleCalculatedFieldStateTest.java | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index ac9a6a929b..0383c5890c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -57,6 +57,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, this.ctx = ctx; this.actorCtx = actorCtx; this.requiredArguments = ctx.getArgNames(); + this.readinessStatus = checkReadiness(requiredArguments, arguments); } @Override @@ -94,11 +95,11 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, } - if (updatedArguments != null) { - readinessStatus = checkReadiness(requiredArguments, arguments); - return updatedArguments; + if (updatedArguments == null) { + return Collections.emptyMap(); } - return Collections.emptyMap(); + readinessStatus = checkReadiness(requiredArguments, arguments); + return updatedArguments; } @Override @@ -153,6 +154,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, } public ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { + if (currentArguments == null) { + return new ReadinessStatus(requiredArguments); + } List emptyArguments = null; for (String requiredArgumentKey : requiredArguments) { ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index cc22797593..4f589572e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -50,6 +50,7 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState this.actorCtx = actorCtx; this.requiredArguments = new ArrayList<>(ctx.getArgNames()); requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); + this.readinessStatus = checkReadiness(requiredArguments, arguments); if (ctx.isApplyExpressionForResolvedArguments()) { this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index a91834a798..ebda120769 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -200,7 +200,7 @@ public class GeofencingCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus()).isNull(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactlyInAnyOrderElementsOf(state.getRequiredArguments()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index a8dcc18025..39827a34fb 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -161,7 +161,7 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus()).isNull(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactlyInAnyOrderElementsOf(state.getRequiredArguments()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 7748186919..9aa3d60ec6 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -204,7 +204,7 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus()).isNull(); + assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactlyInAnyOrderElementsOf(state.getRequiredArguments()); } @Test From 7c207f458cc11d2dc69cac911292dc2a76ceabaf Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 27 Oct 2025 18:12:26 +0200 Subject: [PATCH 6/8] remove null check --- .../calculatedField/CalculatedFieldEntityMessageProcessor.java | 2 +- .../server/service/cf/ctx/state/BaseCalculatedFieldState.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 fb0f92c2ab..aef81474ec 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 @@ -416,7 +416,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } else { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { - String errorMsg = ctx.isInitialized() && state.getReadinessStatus() != null ? state.getReadinessStatus().stringValue() : "Calculated field state is not initialized!"; + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().stringValue() : "Calculated field state is not initialized!"; systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 0383c5890c..fa54b11e9f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -112,7 +112,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, @Override public boolean isReady() { - return readinessStatus != null && readinessStatus.isReady(); + return readinessStatus.isReady(); } @Override From 0c409855ec2c9f8e9e1f0f7b7444e014aa18f563 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 29 Oct 2025 11:05:43 +0200 Subject: [PATCH 7/8] fix modifier typo --- .../server/service/cf/ctx/state/BaseCalculatedFieldState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index fa54b11e9f..7fdf396adf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -153,7 +153,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } - public ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { + protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { if (currentArguments == null) { return new ReadinessStatus(requiredArguments); } From a244d7088f03c57016eb332e2e39ed0d39e7db21 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 30 Oct 2025 14:30:56 +0200 Subject: [PATCH 8/8] Updated logic due to comments --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- .../ctx/state/BaseCalculatedFieldState.java | 6 ++--- .../cf/ctx/state/CalculatedFieldState.java | 26 +++++++------------ .../GeofencingCalculatedFieldStateTest.java | 6 ++--- .../PropagationCalculatedFieldStateTest.java | 6 ++--- .../state/ScriptCalculatedFieldStateTest.java | 6 ++--- .../state/SimpleCalculatedFieldStateTest.java | 6 ++--- 7 files changed, 25 insertions(+), 33 deletions(-) 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 f727b80e80..4966b8132e 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 @@ -482,7 +482,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } else { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { - String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().stringValue() : "Calculated field state is not initialized!"; + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!"; systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); } callback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 033560728e..20f944e433 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -117,7 +117,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, @Override public boolean isReady() { - return readinessStatus.isReady(); + return readinessStatus.ready(); } @Override @@ -162,7 +162,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { if (currentArguments == null) { - return new ReadinessStatus(requiredArguments); + return ReadinessStatus.from(requiredArguments); } List emptyArguments = null; for (String requiredArgumentKey : requiredArguments) { @@ -174,7 +174,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, emptyArguments.add(requiredArgumentKey); } } - return new ReadinessStatus(emptyArguments); + return ReadinessStatus.from(emptyArguments); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 1a5577e431..c649ab5dbf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -16,17 +16,14 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; -import lombok.AllArgsConstructor; -import lombok.Data; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; @@ -101,22 +98,17 @@ public interface CalculatedFieldState extends Closeable { } } - @Data - @AllArgsConstructor - @JsonInclude(JsonInclude.Include.NON_NULL) - class ReadinessStatus { + record ReadinessStatus(boolean ready, String errorMsg) { - private List emptyArguments; + private static final String ERROR_MESSAGE = "Required arguments are missing: "; + private static final ReadinessStatus READY = new ReadinessStatus(true, null); - public boolean isReady() { - return emptyArguments == null || emptyArguments.isEmpty(); - } - - public String stringValue() { - return JacksonUtil.toString(this); + public static ReadinessStatus from(List emptyOrMissingArguments) { + if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { + return ReadinessStatus.READY; + } + return new ReadinessStatus(false, ERROR_MESSAGE + String.join(", ", emptyOrMissingArguments)); } - - } } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index ebda120769..d144fe4dcc 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -200,7 +200,7 @@ public class GeofencingCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactlyInAnyOrderElementsOf(state.getRequiredArguments()); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test @@ -212,7 +212,7 @@ public class GeofencingCalculatedFieldStateTest { "restrictedZones", geofencingRestrictedZoneArgEntry ), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test @@ -224,7 +224,7 @@ public class GeofencingCalculatedFieldStateTest { "restrictedZones", new GeofencingArgumentEntry() ), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).contains("restrictedZones"); + assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index a03be03f59..ddb9f378b0 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -131,7 +131,7 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); } @Test @@ -140,7 +140,7 @@ public class PropagationCalculatedFieldStateTest { state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly(PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); } @Test @@ -148,7 +148,7 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 39827a34fb..51e633a232 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -161,21 +161,21 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactlyInAnyOrderElementsOf(state.getRequiredArguments()); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { state.update(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { state.update(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly("deviceTemperature"); + assertThat(state.getReadinessStatus().errorMsg()).contains("deviceTemperature"); } private TsRollingArgumentEntry createRollingArgEntry() { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 9aa3d60ec6..a64f1e4c60 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -204,7 +204,7 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactlyInAnyOrderElementsOf(state.getRequiredArguments()); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test @@ -215,7 +215,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry ), ctx); assertThat(state.isReady()).isTrue(); - assertThat(state.getReadinessStatus().getEmptyArguments()).isNull(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test @@ -226,7 +226,7 @@ public class SimpleCalculatedFieldStateTest { "key3", new SingleValueArgumentEntry() ), ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().getEmptyArguments()).containsExactly("key3"); + assertThat(state.getReadinessStatus().errorMsg()).contains("key3"); } private CalculatedField getCalculatedField() {