diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 1700b25dbb..66243461e2 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -21,8 +21,8 @@ SET profile_data = jsonb_set( profile_data, '{configuration}', jsonb_build_object( - 'minAllowedScheduledUpdateIntervalInSecForCF', 60, - 'maxRelationLevelPerCfArgument', 10, + 'minAllowedScheduledUpdateIntervalInSecForCF', 10, + 'maxRelationLevelPerCfArgument', 2, 'maxRelatedEntitiesToReturnPerCfArgument', 100, 'minAllowedDeduplicationIntervalInSecForCF', 10, 'minAllowedAggregationIntervalInSecForCF', 60, 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 e6dc7cbcd3..2f4bd0db22 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 @@ -384,6 +384,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware EntityId mainId, MultipleTbCallback parentCallback, TriConsumer relationAction) { + if (isMyPartition(mainId, parentCallback)) { + return; + } List cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId); if (cfsByEntityIdAndProfile.isEmpty()) { parentCallback.onSuccess(); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index ee73b3c1a7..c0f7b9eb40 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -161,8 +161,8 @@ public class TenantProfileController extends BaseController { " \"warnThreshold\": 0,\n" + " \"maxCalculatedFieldsPerEntity\": 5,\n" + " \"maxArgumentsPerCF\": 10,\n" + - " \"minAllowedScheduledUpdateIntervalInSecForCF\": 60,\n" + - " \"maxRelationLevelPerCfArgument\": 10,\n" + + " \"minAllowedScheduledUpdateIntervalInSecForCF\": 10,\n" + + " \"maxRelationLevelPerCfArgument\": 2,\n" + " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxStateSizeInKBytes\": 32,\n" + 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 e5215eca28..84a0ab008f 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 @@ -129,6 +129,8 @@ public class CalculatedFieldCtx implements Closeable { private long cfCheckReevaluationIntervalMillis; private long alarmReevaluationIntervalMillis; private long maxRelatedEntitiesPerCfArgument; + private long minScheduledUpdateIntervalMillis; + private long minDeduplicationIntervalMillis; private Argument propagationArgument; private boolean applyExpressionForResolvedArguments; @@ -312,6 +314,8 @@ public class CalculatedFieldCtx implements Closeable { this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getCfReevaluationCheckInterval()); this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getAlarmsReevaluationInterval()); this.maxRelatedEntitiesPerCfArgument = config.getMaxRelatedEntitiesToReturnPerCfArgument(); + this.minScheduledUpdateIntervalMillis = TimeUnit.SECONDS.toMillis(config.getMinAllowedScheduledUpdateIntervalInSecForCF()); + this.minDeduplicationIntervalMillis = TimeUnit.SECONDS.toMillis(config.getMinAllowedDeduplicationIntervalInSecForCF()); }); } @@ -763,9 +767,7 @@ public class CalculatedFieldCtx implements Closeable { } public boolean hasRelatedEntities() { - return CalculatedFieldType.GEOFENCING == cfType - || CalculatedFieldType.PROPAGATION == cfType - || CalculatedFieldType.RELATED_ENTITIES_AGGREGATION == cfType; + return cfHasRelationPathQuerySource; } public boolean shouldFetchRelatedEntities(CalculatedFieldState state) { @@ -781,7 +783,7 @@ public class CalculatedFieldCtx implements Closeable { if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) { return true; } - return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - Math.max(scheduledUpdateIntervalMillis, minScheduledUpdateIntervalMillis); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java new file mode 100644 index 0000000000..7d5bc47cc0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/HasEntityLimit.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 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.service.cf.ctx.state; + +public interface HasEntityLimit { + + default void checkEntityLimit(int currentEntitiesCount, CalculatedFieldCtx ctx) { + if (currentEntitiesCount >= ctx.getMaxRelatedEntitiesPerCfArgument()) { + throw new IllegalArgumentException( + "Exceeded the maximum allowed related entities per argument '" + + ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration." + ); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index cb64e03d90..868e8a15ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -183,7 +183,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat } public void scheduleReevaluation() { - ScheduledFuture future = ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); + ScheduledFuture future = ctx.scheduleReevaluation(getEnforcedDeduplicationIntervalMillis(), actorCtx); if (future != null) { reevaluationFuture = future; } @@ -209,11 +209,15 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat } private boolean shouldRecalculate() { - boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - deduplicationIntervalMs; + boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - getEnforcedDeduplicationIntervalMillis(); boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; return intervalPassed && argsUpdatedDuringInterval; } + private long getEnforcedDeduplicationIntervalMillis() { + return Math.max(deduplicationIntervalMs, ctx.getMinDeduplicationIntervalMillis()); + } + private Map> prepareInputs() { Map> inputs = new HashMap<>(); for (Map.Entry argEntry : arguments.entrySet()) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java index 7392ff1e0f..9cd40e4250 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.HasEntityLimit; import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; @@ -34,7 +35,7 @@ import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldSta @Data @AllArgsConstructor -public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs { +public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs, HasEntityLimit { private final Map entityInputs; @@ -66,18 +67,18 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs @Override public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { - checkRelatedEntitiesNumber(ctx); + checkEntityLimit(entityInputs.size(), ctx); entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { if (entry.isForceResetPrevious()) { - checkRelatedEntitiesNumber(ctx); + checkEntityLimit(entityInputs.size(), ctx); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); } else { ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); if (argumentEntry != null) { argumentEntry.updateEntry(singleValueArgumentEntry, ctx); } else { - checkRelatedEntitiesNumber(ctx); + checkEntityLimit(entityInputs.size(), ctx); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); } } @@ -87,15 +88,6 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs return true; } - private void checkRelatedEntitiesNumber(CalculatedFieldCtx ctx) { - if (entityInputs.size() >= ctx.getMaxRelatedEntitiesPerCfArgument()) { - throw new IllegalArgumentException( - "Exceeded the maximum allowed related entities per argument '" - + ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration." - ); - } - } - @Override public boolean isEmpty() { return entityInputs.isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java index 9fe51bbf22..cd61509c18 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java @@ -51,9 +51,10 @@ public class GeofencingZoneState { this.ts = attributeKvEntry.getLastUpdateTs(); this.version = attributeKvEntry.getVersion(); if (entry.getValueAsString() == null) { - throw new IllegalArgumentException("Perimeter attribute key '" + entry.getKey() + "' not found for Zone with id: " + zoneId); + throw new IllegalArgumentException("Perimeter attribute '" + entry.getKey() + "' not found for Zone with id: " + zoneId); } - this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class); + this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class, + "Invalid perimeter definition format for Zone with id: " + zoneId + ". Failed to parse attribute '" + entry.getKey() + "'"); } public GeofencingZoneState(GeofencingZoneProto proto) { 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 c3b5951da6..8fdc105168 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.HasEntityLimit; import java.util.ArrayList; import java.util.Collection; @@ -30,7 +31,7 @@ import java.util.List; import java.util.Set; @Data -public class PropagationArgumentEntry implements ArgumentEntry { +public class PropagationArgumentEntry implements ArgumentEntry, HasEntityLimit { private Set entityIds; private transient List added; @@ -93,12 +94,7 @@ public class PropagationArgumentEntry implements ArgumentEntry { private boolean checkAdded(Collection updatedIds, CalculatedFieldCtx ctx) { for (EntityId id : updatedIds) { - if (entityIds.size() >= ctx.getMaxRelatedEntitiesPerCfArgument()) { - throw new IllegalArgumentException( - "Exceeded the maximum allowed related entities per argument '" - + ctx.getMaxRelatedEntitiesPerCfArgument() + "'. Increase the limit in the tenant profile configuration." - ); - } + checkEntityLimit(entityIds.size(), ctx); if (entityIds.add(id)) { if (added == null) { added = new ArrayList<>(); 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 06b632f659..a20c7b0253 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 @@ -25,6 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; @@ -44,8 +45,9 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; @@ -54,6 +56,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -92,7 +95,9 @@ public class GeofencingCalculatedFieldStateTest { private CalculatedFieldCtx ctx; @Mock - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + @Mock + private TbTenantProfileCache tenantProfileCache; @Mock private RelationService relationService; @InjectMocks @@ -100,7 +105,8 @@ public class GeofencingCalculatedFieldStateTest { @BeforeEach void setUp() { - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); state = new GeofencingCalculatedFieldState(ctx.getEntityId()); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index 2e550b8b74..d7bc39f6e8 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -177,7 +177,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L); assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) .isExactlyInstanceOf(IllegalArgumentException.class) - .hasMessage("The given string value cannot be transformed to Json object: someString"); + .hasMessage("Invalid perimeter definition format for Zone with id: " + ZONE_1_ID + ". Failed to parse attribute 'zone'"); } @Test @@ -185,7 +185,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) .isExactlyInstanceOf(IllegalArgumentException.class) - .hasMessage("The given string value cannot be transformed to Json object: \"{}\""); + .hasMessage("Invalid perimeter definition format for Zone with id: " + ZONE_1_ID + ". Failed to parse attribute 'zone'"); } } 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 44154d26dd..66111caf9c 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 @@ -29,6 +29,7 @@ import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -45,8 +46,9 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.DefaultStatsFactory; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; @@ -106,7 +108,10 @@ public class PropagationCalculatedFieldStateTest { private TbelInvokeService tbelInvokeService; @MockitoBean - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + + @MockitoBean + private TbTenantProfileCache tenantProfileCache; @MockitoBean private ActorSystemContext actorSystemContext; @@ -117,9 +122,10 @@ public class PropagationCalculatedFieldStateTest { @BeforeEach void setUp() { when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); - when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); when(actorSystemContext.getCalculatedFieldProcessingService()).thenReturn(cfProcessingService); - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + when(actorSystemContext.getTenantProfileCache()).thenReturn(tenantProfileCache); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); } void initCtxAndState(boolean applyExpressionToResolvedArguments) { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java index fba53c278a..cbde708ff8 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java @@ -25,6 +25,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -46,14 +47,16 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.DefaultStatsFactory; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -79,7 +82,10 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest { private TbelInvokeService tbelInvokeService; @MockitoBean - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + + @MockitoBean + private TbTenantProfileCache tenantProfileCache; @MockitoBean private ActorSystemContext actorSystemContext; @@ -87,8 +93,9 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest { @BeforeEach void setUp() { when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); - when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + when(actorSystemContext.getTenantProfileCache()).thenReturn(tenantProfileCache); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); initCtxAndState(); } 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 52af3f564f..e1b881fec1 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 @@ -27,6 +27,7 @@ import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -41,13 +42,15 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.DefaultStatsFactory; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -75,15 +78,19 @@ public class ScriptCalculatedFieldStateTest { private TbelInvokeService tbelInvokeService; @MockitoBean - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + + @MockitoBean + private TbTenantProfileCache tenantProfileCache; @BeforeEach void setUp() { ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); - when(systemContext.getApiLimitService()).thenReturn(apiLimitService); + when(systemContext.getTenantProfileCache()).thenReturn(tenantProfileCache); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); state = new ScriptCalculatedFieldState(ctx.getEntityId()); 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 dc91523e74..4454d41e6e 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 @@ -24,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -40,12 +41,14 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -69,13 +72,16 @@ public class SimpleCalculatedFieldStateTest { private CalculatedFieldCtx ctx; @Mock - private ApiLimitService apiLimitService; + private TenantProfile tenantProfile; + @Mock + private TbTenantProfileCache tenantProfileCache; @InjectMocks private ActorSystemContext systemContext; @BeforeEach void setUp() { - when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + when(tenantProfileCache.get(any(TenantId.class))).thenReturn(tenantProfile); + when(tenantProfile.getProfileConfiguration()).thenReturn(Optional.of(new DefaultTenantProfileConfiguration())); ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); state = new SimpleCalculatedFieldState(ctx.getEntityId()); 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 dd3c7b1a7d..a04816365b 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 @@ -172,12 +172,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; - @Schema(example = "60") - private int minAllowedScheduledUpdateIntervalInSecForCF = 60; - @Builder.Default @Schema(example = "10") + private int minAllowedScheduledUpdateIntervalInSecForCF = 10; + @Builder.Default + @Schema(example = "2") @Positive - private int maxRelationLevelPerCfArgument = 10; + private int maxRelationLevelPerCfArgument = 2; @Builder.Default @Schema(example = "100") @Positive diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 9913210c1a..2cf758885f 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -115,10 +115,15 @@ public class JacksonUtil { @Contract("null, _ -> null") // so that IDE doesn't show NPE warning when input is not null public static T fromString(String string, Class clazz) { + return fromString(string, clazz, "The given string value cannot be transformed to Json object: " + string); + } + + @Contract("null, _, _ -> null") // so that IDE doesn't show NPE warning when input is not null + public static T fromString(String string, Class clazz, String errorMsg) { try { return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; } catch (IOException e) { - throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + string, e); + throw new IllegalArgumentException(errorMsg, e); } } diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java index 9ddddbdf9e..1c22d6a007 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -32,6 +33,9 @@ public class PerimeterDefinitionDeserializer extends JsonDeserializer JacksonUtil.fromString(badJson, PerimeterDefinition.class, customError)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(customError) + .hasCauseInstanceOf(JsonMappingException.class) + .rootCause() + .hasMessageContaining("CirclePerimeterDefinition missing required fields"); + } + + @Test + void shouldThrowWhenJsonIsGarbage() { + String garbageJson = "\"NotAnObjectOrArray\""; + String customError = "Garbage check"; + + assertThatThrownBy(() -> JacksonUtil.fromString(garbageJson, PerimeterDefinition.class, customError)) + .isInstanceOf(IllegalArgumentException.class) + .hasCauseInstanceOf(JsonMappingException.class) + .rootCause() + .hasMessageContaining("Unknown JSON format"); + } + + @Test + void shouldReturnNullWhenInputIsNull() { + //noinspection ConstantConditions + PerimeterDefinition result = JacksonUtil.fromString(null, PerimeterDefinition.class, "Error"); + assertThat(result).isNull(); + } + } diff --git a/msa/edqs/docker/start-tb-edqs.sh b/msa/edqs/docker/start-tb-edqs.sh index 40970ead21..4968f5b866 100755 --- a/msa/edqs/docker/start-tb-edqs.sh +++ b/msa/edqs/docker/start-tb-edqs.sh @@ -15,11 +15,19 @@ # limitations under the License. # -CONF_FOLDER=${pkg.installFolder}/conf jarfile=${pkg.installFolder}/bin/${pkg.name}.jar configfile=${pkg.name}.conf -source "${CONF_FOLDER}/${configfile}" +CONF_FOLDER="/config" +if [ -d "${CONF_FOLDER}" ]; then + LOGGING_CONFIG="${CONF_FOLDER}/logback.xml" + source "${CONF_FOLDER}/${configfile}" + export LOADER_PATH=${CONF_FOLDER},${LOADER_PATH} +else + CONF_FOLDER="/usr/share/${pkg.name}/conf" + LOGGING_CONFIG="/usr/share/${pkg.name}/conf/logback.xml" + source "${CONF_FOLDER}/${configfile}" +fi echo "Starting '${project.name}' ..." @@ -27,5 +35,5 @@ cd ${pkg.installFolder}/bin exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.edqs.ThingsboardEdqsApplication \ -Dspring.jpa.hibernate.ddl-auto=none \ - -Dlogging.config=$CONF_FOLDER/logback.xml \ + -Dlogging.config=${LOGGING_CONFIG} \ org.springframework.boot.loader.launch.PropertiesLauncher diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 68a9b6759d..bf57dd4441 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -34,7 +34,7 @@ import { } from '@shared/models/js-function.models'; import { DomSanitizer } from '@angular/platform-browser'; import { SecurityContext } from '@angular/core'; -import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; const varsRegex = /\${([^}]*)}/g; const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; @@ -1084,3 +1084,12 @@ export const validateEmail = (control: AbstractControl): ValidationErrors | null return emailRegex.test(control.value) ? null : {email: true}; }; +export const objectRequired = (): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (value && !isObject(value)) { + return { objectRequired: true }; + } + return null; + }; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 42528f2d03..cadac572c6 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -109,7 +109,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig
+ api-key.expiration-date @for (value of expirationTimeOptions; track value) { diff --git a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts index d3102d9fe6..b2b26ee51e 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts @@ -81,7 +81,7 @@ export class ApiKeysTableConfig extends EntityTableConfig { new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), new EntityTableColumn('description', 'api-key.description', '100%', (entity) => this.customTranslate.transform(entity?.description), () => ({}), true, () => ({}), - (entity) => entity?.description.length > 80 ? this.customTranslate.transform(entity.description) : undefined, false, + (entity) => entity?.description.length > 40 ? this.customTranslate.transform(entity.description) : undefined, false, { name: this.translate.instant('api-key.edit-description'), icon: 'edit', diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts index 20ebe549a2..425069239f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -124,13 +124,14 @@ export class CalculatedFieldComponent extends EntityComponent { this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); + this.entityForm.get('type').updateValueAndValidity(); }); } onTestScript(expression?: string): Observable { return this.cfFormService.testScript( this.entity?.id?.id, - this.entityFormValue(), + this.entityValue, this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), this.destroyRef, expression diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 972b74a9f7..a9924909b7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -186,6 +186,9 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.toggleScopeByOutputType(this.outputForm.get('type').value); this.updatedStrategy(); this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value); + if (this.outputForm.invalid) { + this.outputForm.updateValueAndValidity(); + } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index 4eac964538..1cf2476c2e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -143,7 +143,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, this.updatedFormWithScript(); } setTimeout(() => { - this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true, emitEvent: false}); + this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); }); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index fd86a65621..fd2e0783af 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -155,6 +155,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); setTimeout(() => { this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + this.simpleConfiguration.get('output').updateValueAndValidity({onlySelf: true}); }); } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts index 4bb4b06839..207eb19253 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts @@ -218,6 +218,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, createTenantProfile($event: Event, profileName: string) { $event.preventDefault(); + $event.stopPropagation(); const tenantProfile: TenantProfile = { name: profileName }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html index 36f9b78bb0..582070ad33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -166,11 +166,11 @@
- + description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index d1f7ac7e97..9e6d153786 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -216,7 +216,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI } }); this.twoFaFormGroup.patchValue(processFormValue); - this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter.tenantProfilesIds); + this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter?.tenantProfilesIds); this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').patchValue(this.filterByTenants, {onlySelf: true}); } diff --git a/ui-ngx/src/app/shared/components/country-autocomplete.component.html b/ui-ngx/src/app/shared/components/country-autocomplete.component.html index 9b3857cafb..befb160df6 100644 --- a/ui-ngx/src/app/shared/components/country-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/country-autocomplete.component.html @@ -15,14 +15,19 @@ limitations under the License. --> - + {{ labelText }} + @if (countryFormGroup.get('country').value?.flag) { + + {{ countryFormGroup.get('country').value.flag }} + + }