diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 320d3e5bdd..0add4c0545 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -27,7 +27,7 @@ SET profile_data = jsonb_set( CASE WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' THEN NULL - ELSE to_jsonb(3600) + ELSE to_jsonb(60) END, 'maxRelationLevelPerCfArgument', CASE diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java deleted file mode 100644 index 301fe22dfb..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.calculatedField; - -import lombok.Data; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; - -@Data -public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { - - private final TenantId tenantId; - private final CalculatedFieldId cfId; - - @Override - public MsgType getMsgType() { - return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 2a5f3c3cfd..c57984ef3d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG: - processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg); - break; default: return false; } 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 7513ca41e2..f8b61a082f 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 @@ -49,6 +49,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.ArrayList; import java.util.Collection; @@ -227,18 +228,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException { - log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId()); - CalculatedFieldState currentState = states.get(msg.getCfId()); - if (currentState == null) { - log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId()); - } else { - currentState.setDirty(true); - log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId()); - } - msg.getCallback().onSuccess(); - } - private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } @@ -266,12 +255,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state == null) { state = getOrInitState(ctx); justRestored = true; - } else if (state.isDirty()) { - log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId()); + } else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) { + log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId()); try { Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); - state.setDirty(false); + var geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); } catch (Exception e) { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } @@ -403,7 +393,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); } - private Map mapToArguments(EntityId entityId, Map argNames, List geoArgNames, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map argNames, List geofencingArgNames, AttributeScopeProto scope, List attrDataList) { Map arguments = new HashMap<>(); for (AttributeValueProto item : attrDataList) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); @@ -411,7 +401,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (argName == null) { continue; } - if (geoArgNames.contains(argName)) { + if (geofencingArgNames.contains(argName)) { arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); continue; } @@ -425,26 +415,32 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (argNames.isEmpty()) { return Collections.emptyMap(); } - return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys); + List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames(); + return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys); } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List removedAttrKeys) { - return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys); + return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys); } - private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, AttributeScopeProto scope, List removedAttrKeys) { + private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, List geofencingArgNames, AttributeScopeProto scope, List removedAttrKeys) { Map arguments = new HashMap<>(); for (String removedKey : removedAttrKeys) { ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); String argName = argNames.get(key); - if (argName != null) { - Argument argument = configArguments.get(argName); - String defaultValue = (argument != null) ? argument.getDefaultValue() : null; - arguments.put(argName, StringUtils.isNotEmpty(defaultValue) - ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) - : new SingleValueArgumentEntry()); - + if (argName == null) { + continue; } + if (geofencingArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry()); + continue; + } + Argument argument = configArguments.get(argName); + String defaultValue = (argument != null) ? argument.getDefaultValue() : null; + arguments.put(argName, StringUtils.isNotEmpty(defaultValue) + ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) + : new SingleValueArgumentEntry()); + } return arguments; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index ab6cb34176..7d2ae0ff44 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -79,9 +79,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG: - processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg); - break; default: return false; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 75ca0b4c9b..7a76cb9821 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -57,10 +56,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -74,7 +70,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); - private final Map> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>(); private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; @@ -113,8 +108,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); - cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true)); - cfDynamicArgumentsRefreshTasks.clear(); ctx.stop(ctx.getSelf()); } @@ -274,7 +267,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); addLinks(cf); - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb)); } } @@ -304,12 +296,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.put(newCf.getId(), newCfCtx); List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); - boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); - if (hasSchedulingConfigChanges) { - cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false); - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx); - } - List newCfList = new CopyOnWriteArrayList<>(); boolean found = false; for (CalculatedFieldCtx oldCtx : oldCfList) { @@ -350,19 +336,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); deleteLinks(cfCtx); - cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); } - private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { - var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId); - if (existingTask != null) { - existingTask.cancel(false); - String reason = cfDeleted ? "deletion" : "update"; - log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason); - } - } - public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); log.debug("Received telemetry msg from entity [{}]", entityId); @@ -442,43 +418,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } - private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { - CalculatedField cf = cfCtx.getCalculatedField(); - if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) { - return; - } - if (!scheduledCfConfig.isScheduledUpdateEnabled()) { - return; - } - if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) { - log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); - return; - } - long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval()); - var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); - - ScheduledFuture scheduledFuture = systemContext - .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); - cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture); - log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId()); - } - - public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) { - log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId()); - CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId()); - if (cfCtx == null) { - log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId()); - cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true); - return; - } - applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb)); - } - - private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { - log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback)); - } - private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(msg); @@ -565,7 +504,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); } private void initCalculatedFieldLink(CalculatedFieldLink link) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java deleted file mode 100644 index fdf864611f..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.actors.calculatedField; - -import lombok.Data; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; - -@Data -public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { - - private final TenantId tenantId; - private final CalculatedFieldId cfId; - private final TbCallback callback; - - @Override - public MsgType getMsgType() { - return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 45305ca9e3..8709e0cb68 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -45,6 +45,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.HashMap; import java.util.List; @@ -102,6 +103,9 @@ public abstract class AbstractCalculatedFieldProcessingService { return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); result.updateState(ctx, resolveArgumentFutures(argFutures)); + if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) { + geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); + } return result; }, MoreExecutors.directExecutor()); } 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 9a1d06cf24..6d877331bd 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 @@ -62,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { boolean entryUpdated; if (existingEntry == null || newEntry.isForceResetPrevious()) { - validateNewEntry(newEntry); + validateNewEntry(key, newEntry); arguments.put(key, newEntry); entryUpdated = true; } else { @@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } - protected void validateNewEntry(ArgumentEntry newEntry) {} + protected void validateNewEntry(String key, ArgumentEntry newEntry) {} private void updateLastUpdateTimestamp(ArgumentEntry entry) { long newTs = this.latestTimestamp; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index c2cc083853..13012a028a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -43,11 +43,13 @@ import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; @@ -78,9 +80,12 @@ public class CalculatedFieldCtx { private long maxStateSize; private long maxSingleValueArgumentSize; + private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; private List linkedEntityGeofencingArgumentNames; + private long scheduledUpdateIntervalMillis; + public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { this.calculatedField = calculatedField; @@ -101,6 +106,7 @@ public class CalculatedFieldCtx { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); if (refId == null && entry.getValue().hasDynamicSource()) { + relationQueryDynamicArguments = true; continue; } if (refId == null || refId.equals(calculatedField.getEntityId())) { @@ -126,6 +132,9 @@ public class CalculatedFieldCtx { }); } } + if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { + this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L; + } this.tbelInvokeService = tbelInvokeService; this.relationService = relationService; @@ -329,25 +338,42 @@ public class CalculatedFieldCtx { public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression); boolean outputChanged = !output.equals(other.output); - return expressionChanged || outputChanged; + boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis; + return expressionChanged || outputChanged || scheduledUpdatesConfigChanged; } public boolean hasStateChanges(CalculatedFieldCtx other) { boolean typeChanged = !cfType.equals(other.cfType); boolean argumentsChanged = !arguments.equals(other.arguments); - return typeChanged || argumentsChanged; + boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other); + return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged; } - public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { - if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { - boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); - boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval(); - return refreshTriggerChanged || refreshIntervalChanged; + private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); } return false; } + public boolean hasRelationQueryDynamicArguments() { + return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1; + } + + public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) { + if (!hasRelationQueryDynamicArguments()) { + return false; + } + if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) { + return false; + } + if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) { + return true; + } + return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + } + public String getSizeExceedsLimitMessage() { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } 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 5f8e7538c4..3e0964bfd2 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 @@ -50,13 +50,6 @@ public interface CalculatedFieldState { long getLatestTimestamp(); - default void setDirty(boolean dirty) { - } - - default boolean isDirty() { - return false; - } - void setRequiredArguments(List requiredArguments); boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 80b650fc7c..5a437b52f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -48,9 +48,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { + protected void validateNewEntry(String key, ArgumentEntry newEntry) { if (newEntry instanceof TsRollingArgumentEntry) { - throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields."); + throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " + + "Rolling argument entry is not supported for simple calculated fields."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index f526cc00ab..53e5c19e72 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry { } public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { - this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry))); + this(Map.of(entityId, ProtoUtils.fromProto(entry))); } public GeofencingArgumentEntry(Map entityIdkvEntryMap) { @@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); } + if (geofencingArgumentEntry.isEmpty()) { + zoneStates.clear(); + return true; + } boolean updated = false; for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { if (updateZone(zoneEntry)) { @@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { zoneStates.put(zoneId, newZoneState); return true; } + if (newZoneState.getPerimeterDefinition() == null) { + zoneStates.remove(zoneId); + return true; + } return existingZoneState.update(newZoneState); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index 506ddcff78..398de0c20b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -15,16 +15,19 @@ */ package org.thingsboard.server.service.cf.ctx.state.geofencing; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; @@ -39,7 +42,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -51,18 +53,14 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo @Data @Slf4j +@NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { - private boolean dirty; + private long lastDynamicArgumentsRefreshTs = -1; - public GeofencingCalculatedFieldState() { - super(new ArrayList<>(), new HashMap<>(), false, -1); - this.dirty = false; - } - - public GeofencingCalculatedFieldState(List argNames) { - super(argNames); + public GeofencingCalculatedFieldState(List requiredArguments) { + super(requiredArguments); } @Override @@ -71,49 +69,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { - if (arguments == null) { - arguments = new HashMap<>(); - } - - boolean stateUpdated = false; - - for (var entry : argumentValues.entrySet()) { - String key = entry.getKey(); - ArgumentEntry newEntry = entry.getValue(); - - checkArgumentSize(key, newEntry, ctx); - - ArgumentEntry existingEntry = arguments.get(key); - boolean entryUpdated; - - if (existingEntry == null || newEntry.isForceResetPrevious()) { - entryUpdated = switch (key) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { - if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) { - throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + - "Only SINGLE_VALUE type is allowed."); - } - arguments.put(key, singleValueArgumentEntry); - yield true; - } - default -> { - if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { - throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + - "Only GEOFENCING type is allowed."); - } - arguments.put(key, geofencingArgumentEntry); - yield true; - } - }; - } else { - entryUpdated = existingEntry.updateEntry(newEntry); + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only SINGLE_VALUE type is allowed."); + } } - if (entryUpdated) { - stateUpdated = true; + default -> { + if (!(newEntry instanceof GeofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only GEOFENCING type is allowed."); + } } } - return stateUpdated; } @Override @@ -125,7 +95,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); Map zoneGroups = geofencingCfg.getZoneGroups(); - ObjectNode resultNode = JacksonUtil.newObjectNode(); + ObjectNode valuesNode = JacksonUtil.newObjectNode(); List> relationFutures = new ArrayList<>(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { @@ -154,10 +124,11 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { relationFutures.add(f); } }); - updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode); + updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode); }); - var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); + OutputType outputType = ctx.getOutput().getType(); + var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode)); if (relationFutures.isEmpty()) { return Futures.immediateFuture(result); } @@ -171,7 +142,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); } - private void updateResultNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { + private void updateValuesNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); final String eventKey = argumentKey + "Event"; final String statusKey = argumentKey + "Status"; @@ -185,6 +156,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } } + private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) { + if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) { + return valuesNode; + } + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", latestTimestamp); + resultNode.set("values", valuesNode); + return resultNode; + } + private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); boolean prevInside = zoneResults.stream() diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index b6a1faa1ed..ffd876575c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -831,10 +831,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); assertThat(foundTenantProfile).isNotNull(); assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); - foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10); + int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10; + foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF); TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); assertThat(savedTenantProfile).isNotNull(); - assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10); + assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF); loginTenantAdmin(); // --- Arrange entities --- @@ -884,7 +885,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setOutput(out); // Enable scheduled refresh with a 6-second interval - cfg.setScheduledUpdateInterval(6); + cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF); + cfg.setScheduledUpdateEnabled(true); cf.setConfiguration(cfg); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); @@ -935,7 +937,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes relAllowedB.setType("AllowedZone"); doPost("/api/relation", relAllowedB).andExpect(status().isOk()); - awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId()); + awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF); // --- Same coordinates as before, but now we expect ENTERED since a new zone is registered --- doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index fd01581e36..d7c9ad4590 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -155,6 +155,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -1104,14 +1105,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } - protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) { + protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) { CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); - Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> { + Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); - boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty(); - log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty); - return stateDirty; + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() + < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); + return isReady; }); } 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 691a1f7ec4..a86af77555 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 @@ -257,7 +257,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getScope()).isEqualTo(output.getScope()); - assertThat(result2.getResult()).isEqualTo( + assertThat(result2.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesEvent", "LEFT") .put("allowedZonesStatus", "OUTSIDE") @@ -329,7 +329,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getScope()).isEqualTo(output.getScope()); - assertThat(result2.getResult()).isEqualTo( + assertThat(result2.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesEvent", "LEFT") .put("restrictedZonesEvent", "ENTERED") @@ -401,7 +401,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getScope()).isEqualTo(output.getScope()); - assertThat(result2.getResult()).isEqualTo( + assertThat(result2.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesStatus", "OUTSIDE") .put("restrictedZonesStatus", "INSIDE") 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 8c631ecf6f..3aef8896de 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 @@ -123,7 +123,8 @@ public class SimpleCalculatedFieldStateTest { Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); assertThatThrownBy(() -> state.updateState(ctx, newArgs)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Rolling argument entry is not supported for simple calculated fields."); + .hasMessage("Unsupported argument type detected for argument: key3. " + + "Rolling argument entry is not supported for simple calculated fields."); } @Test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index 7902a9cf5b..d0c5786f62 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -15,11 +15,8 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import com.fasterxml.jackson.annotation.JsonIgnore; - public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { - @JsonIgnore boolean isScheduledUpdateEnabled(); int getScheduledUpdateInterval(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index dc331f5876..b331abc50b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -34,6 +34,8 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal private EntityCoordinates entityCoordinates; private Map zoneGroups; + + private boolean scheduledUpdateEnabled; private int scheduledUpdateInterval; private Output output; @@ -61,11 +63,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal return output; } - @Override - public boolean isScheduledUpdateEnabled() { - return scheduledUpdateInterval > 0 && zoneGroups.values().stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); - } - @Override public void validate() { if (entityCoordinates == null) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 4c8b9e06bd..c6bd9a7f38 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -173,7 +173,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Schema(example = "10") private long maxArgumentsPerCF = 10; @Schema(example = "3600") - private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; + private int minAllowedScheduledUpdateIntervalInSecForCF = 60; @Schema(example = "10") private int maxRelationLevelPerCfArgument = 10; @Builder.Default diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index 91a47aac57..c5fb1f8953 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -101,33 +101,6 @@ public class GeofencingCalculatedFieldConfigurationTest { verify(zoneGroupConfigurationB).validate(zoneGroupBName); } - @Test - void scheduledUpdateDisabledWhenIntervalIsZero() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateInterval(0); - assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); - } - - @Test - void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); - when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); - cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock)); - cfg.setScheduledUpdateInterval(60); - assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); - } - - @Test - void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); - when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); - cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock)); - cfg.setScheduledUpdateInterval(60); - assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); - } - @Test void testGetArgumentsOverride() { var cfg = new GeofencingCalculatedFieldConfiguration(); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 48b07af29b..20043582d7 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -147,10 +147,7 @@ public enum MsgType { /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG, - - CF_DYNAMIC_ARGUMENTS_REFRESH_MSG, - CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; + CF_ENTITY_DELETE_MSG; @Getter private final boolean ignoreOnStart; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 49bdcca0fb..7f563ea436 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -99,53 +99,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldNotChangeScheduledInterval() { - // Arrange a device - Device device = createTestDevice(); - - // Build a valid Geofencing configuration - GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); - - // Coordinates: TS_LATEST, no dynamic source - EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); - cfg.setEntityCoordinates(entityCoordinates); - - // Zone-group argument (ATTRIBUTE) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zoneGroupConfiguration.setRefEntityId(device.getId()); - cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); - - // Set a scheduled interval to some value - cfg.setScheduledUpdateInterval(600); - - // Create & save Calculated Field - CalculatedField cf = new CalculatedField(); - cf.setTenantId(tenantId); - cf.setEntityId(device.getId()); - cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF clamp test"); - cf.setConfigurationVersion(0); - cf.setConfiguration(cfg); - - CalculatedField saved = calculatedFieldService.save(cf); - - assertThat(saved).isNotNull(); - assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); - - var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); - - // Assert: the interval is saved, but scheduling is not enabled - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); - boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); - - assertThat(savedInterval).isEqualTo(600); - assertThat(scheduledUpdateEnabled).isFalse(); - - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); - } - - @Test - public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalLessThanMinAllowedIntervalInTenantProfile() { // Arrange a device Device device = createTestDevice(); @@ -165,15 +119,22 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int valueFromConfig = min - 10; + // Enable scheduling with an interval below tenant min - cfg.setScheduledUpdateInterval(600); + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(valueFromConfig); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); cf.setTenantId(tenantId); cf.setEntityId(device.getId()); cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF clamp test"); + cf.setName("GF min allowed scheduled update interval test"); cf.setConfigurationVersion(0); cf.setConfiguration(cfg); @@ -185,7 +146,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelIsGreaterThanMaxAllowedRelationLevelInTenantProfile() { + public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelGreaterThanMaxAllowedRelationLevelInTenantProfile() { // Arrange a device Device device = createTestDevice(); @@ -210,7 +171,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cf.setTenantId(tenantId); cf.setEntityId(device.getId()); cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF clamp test"); + cf.setName("GF max relation level test"); cf.setConfigurationVersion(0); cf.setConfiguration(cfg); @@ -221,7 +182,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldUseScheduledIntervalFromConfig() { + public void testSaveGeofencingCalculatedField_shouldSaveWithoutDataValidationExceptionOnScheduledUpdateInterval() { // Arrange a device Device device = createTestDevice(); @@ -245,10 +206,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() .getMinAllowedScheduledUpdateIntervalInSecForCF(); - + int valueFromConfig = min + 100; // Enable scheduling with an interval greater than tenant min - int valueFromConfig = min + 100; + cfg.setScheduledUpdateEnabled(true); cfg.setScheduledUpdateInterval(valueFromConfig); // Create & save Calculated Field @@ -256,7 +217,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cf.setTenantId(tenantId); cf.setEntityId(device.getId()); cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF no clamp test"); + cf.setName("GF no validation error test"); cf.setConfigurationVersion(0); cf.setConfiguration(cfg); @@ -267,7 +228,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); - // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); assertThat(savedInterval).isEqualTo(valueFromConfig);