added api limits and fixed tests

This commit is contained in:
IrynaMatveieva 2025-02-03 12:01:06 +02:00
parent 7e055ec353
commit 1a67769f1c
19 changed files with 145 additions and 88 deletions

View File

@ -198,20 +198,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return state; return state;
} }
private UUID toTbMsgId(CalculatedFieldTelemetryMsgProto proto) {
if (proto.getTbMsgIdMSB() != 0 && proto.getTbMsgIdLSB() != 0) {
return new UUID(proto.getTbMsgIdMSB(), proto.getTbMsgIdLSB());
}
return null;
}
private TbMsgType toTbMsgType(CalculatedFieldTelemetryMsgProto proto) {
if (!proto.getTbMsgType().isEmpty()) {
return TbMsgType.valueOf(proto.getTbMsgType());
}
return null;
}
@SneakyThrows @SneakyThrows
private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) { private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) {
if (state.isReady()) { if (state.isReady()) {
@ -290,4 +276,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return cfIds; return cfIds;
} }
private UUID toTbMsgId(CalculatedFieldTelemetryMsgProto proto) {
if (proto.getTbMsgIdMSB() != 0 && proto.getTbMsgIdLSB() != 0) {
return new UUID(proto.getTbMsgIdMSB(), proto.getTbMsgIdLSB());
}
return null;
}
private TbMsgType toTbMsgType(CalculatedFieldTelemetryMsgProto proto) {
if (!proto.getTbMsgType().isEmpty()) {
return TbMsgType.valueOf(proto.getTbMsgType());
}
return null;
}
} }

View File

@ -217,6 +217,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
callback.onSuccess(); callback.onSuccess();
} else { } else {
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService()); var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService());
try {
cfCtx.init();
} catch (Exception e) {
if (DebugModeUtil.isDebugAllAvailable(cf)) {
systemContext.persistCalculatedFieldDebugEvent(cf.getTenantId(), cf.getId(), cf.getEntityId(), null, null, null, null, e);
}
}
calculatedFields.put(cf.getId(), cfCtx); calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration. // 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) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
@ -257,6 +264,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// We use copy on write lists to safely pass the reference to another actor for the iteration. // 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) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
if (newCfCtx.hasSignificantChanges(oldCfCtx)) { if (newCfCtx.hasSignificantChanges(oldCfCtx)) {
try {
newCfCtx.init();
} catch (Exception e) {
if (DebugModeUtil.isDebugAllAvailable(newCf)) {
systemContext.persistCalculatedFieldDebugEvent(newCf.getTenantId(), newCf.getId(), newCf.getEntityId(), null, null, null, null, e);
}
}
initCf(newCfCtx, callback, true); initCf(newCfCtx, callback, true);
} }
} }

View File

@ -61,6 +61,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
@ -69,6 +70,7 @@ import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
@ -142,14 +144,13 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
private final TimeseriesService timeseriesService; private final TimeseriesService timeseriesService;
private final CalculatedFieldStateService stateService; private final CalculatedFieldStateService stateService;
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final ApiLimitService apiLimitService;
private ListeningExecutorService calculatedFieldExecutor; private ListeningExecutorService calculatedFieldExecutor;
private ListeningExecutorService calculatedFieldCallbackExecutor; private ListeningExecutorService calculatedFieldCallbackExecutor;
private final ConcurrentMap<CalculatedFieldEntityCtxId, CalculatedFieldEntityCtx> states = new ConcurrentHashMap<>(); private final ConcurrentMap<CalculatedFieldEntityCtxId, CalculatedFieldEntityCtx> states = new ConcurrentHashMap<>();
private static final int MAX_LAST_RECORDS_VALUE = 1024;
private static final Set<EntityType> supportedReferencedEntities = EnumSet.of( private static final Set<EntityType> supportedReferencedEntities = EnumSet.of(
EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT
); );
@ -560,7 +561,8 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
long startTs = currentTime - timeWindow; long startTs = currentTime - timeWindow;
int limit = argument.getLimit() == 0 ? MAX_LAST_RECORDS_VALUE : argument.getLimit(); long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
int limit = argument.getLimit() == 0 ? (int) maxDataPoints : argument.getLimit();
ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE);
ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));

View File

@ -197,7 +197,8 @@ public class CalculatedFieldCtx {
boolean entityIdChanged = !entityId.equals(other.entityId); boolean entityIdChanged = !entityId.equals(other.entityId);
boolean typeChanged = !cfType.equals(other.cfType); boolean typeChanged = !cfType.equals(other.cfType);
boolean argumentsChanged = !arguments.equals(other.arguments); boolean argumentsChanged = !arguments.equals(other.arguments);
return entityIdChanged || typeChanged || argumentsChanged; boolean expressionChanged = !expression.equals(other.expression);
return entityIdChanged || typeChanged || argumentsChanged || expressionChanged;
} }
} }

View File

@ -115,7 +115,7 @@ public class RocksDBStateService implements CalculatedFieldStateService {
singleValueProtoBuilder.setVersion(entry.getVersion()); singleValueProtoBuilder.setVersion(entry.getVersion());
} }
KvEntry value = entry.getValue(); KvEntry value = entry.getKvEntryValue();
if (value != null) { if (value != null) {
singleValueProtoBuilder.setHasV(true) singleValueProtoBuilder.setHasV(true)
.setValue(ProtoUtils.toKeyValueProto(value)); .setValue(ProtoUtils.toKeyValueProto(value));

View File

@ -53,7 +53,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) { for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) {
try { try {
BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getValue(); BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue();
expr.setVariable(entry.getKey(), Double.parseDouble(kvEntry.getValueAsString())); expr.setVariable(entry.getKey(), Double.parseDouble(kvEntry.getValueAsString()));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number.");

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -34,19 +35,19 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
public static final ArgumentEntry EMPTY = new SingleValueArgumentEntry(0); public static final ArgumentEntry EMPTY = new SingleValueArgumentEntry(0);
private long ts; private long ts;
private BasicKvEntry value; private BasicKvEntry kvEntryValue;
private Long version; private Long version;
public SingleValueArgumentEntry(TsKvProto entry) { public SingleValueArgumentEntry(TsKvProto entry) {
this.ts = entry.getTs(); this.ts = entry.getTs();
this.version = entry.getVersion(); this.version = entry.getVersion();
this.value = ProtoUtils.fromProto(entry.getKv()); this.kvEntryValue = ProtoUtils.fromProto(entry.getKv());
} }
public SingleValueArgumentEntry(AttributeValueProto entry) { public SingleValueArgumentEntry(AttributeValueProto entry) {
this.ts = entry.getLastUpdateTs(); this.ts = entry.getLastUpdateTs();
this.version = entry.getVersion(); this.version = entry.getVersion();
this.value = ProtoUtils.basicKvEntryFromProto(entry); this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry);
} }
public SingleValueArgumentEntry(KvEntry entry) { public SingleValueArgumentEntry(KvEntry entry) {
@ -57,7 +58,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
this.ts = attributeKvEntry.getLastUpdateTs(); this.ts = attributeKvEntry.getLastUpdateTs();
this.version = attributeKvEntry.getVersion(); this.version = attributeKvEntry.getVersion();
} }
this.value = ProtoUtils.basicKvEntryFromKvEntry(entry); this.kvEntryValue = ProtoUtils.basicKvEntryFromKvEntry(entry);
} }
/** /**
@ -65,7 +66,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
* */ * */
private SingleValueArgumentEntry(int ignored) { private SingleValueArgumentEntry(int ignored) {
this.ts = System.currentTimeMillis(); this.ts = System.currentTimeMillis();
this.value = null; this.kvEntryValue = null;
} }
@Override @Override
@ -73,9 +74,14 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
return ArgumentEntryType.SINGLE_VALUE; return ArgumentEntryType.SINGLE_VALUE;
} }
@JsonIgnore
public Object getValue() {
return kvEntryValue.getValue();
}
@Override @Override
public ArgumentEntry copy() { public ArgumentEntry copy() {
return new SingleValueArgumentEntry(this.ts, this.value, this.version); return new SingleValueArgumentEntry(this.ts, this.kvEntryValue, this.version);
} }
@Override @Override
@ -88,7 +94,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
Long newVersion = singleValueEntry.getVersion(); Long newVersion = singleValueEntry.getVersion();
if (newVersion == null || this.version == null || newVersion > this.version) { if (newVersion == null || this.version == null || newVersion > this.version) {
this.ts = singleValueEntry.getTs(); this.ts = singleValueEntry.getTs();
this.value = singleValueEntry.getValue(); this.kvEntryValue = singleValueEntry.getKvEntryValue();
this.version = newVersion; this.version = newVersion;
return true; return true;
} }

View File

@ -98,7 +98,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
} }
private boolean updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) { private boolean updateSingleValueEntry(SingleValueArgumentEntry singleValueEntry) {
return addTsRecordIfAbsent(singleValueEntry.getTs(), singleValueEntry.getValue()); return addTsRecordIfAbsent(singleValueEntry.getTs(), singleValueEntry.getKvEntryValue());
} }
private boolean addTsRecordIfAbsent(Long ts, KvEntry value) { private boolean addTsRecordIfAbsent(Long ts, KvEntry value) {

View File

@ -48,9 +48,6 @@ import static org.thingsboard.server.dao.service.Validator.validateEntityId;
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService { public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService {
private static final int MAX_ARGUMENT_SIZE = 10;
private static final int MAX_CALCULATED_FIELD_NUMBER = 10;
private final CalculatedFieldService calculatedFieldService; private final CalculatedFieldService calculatedFieldService;
@Override @Override
@ -62,9 +59,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
CalculatedField existingCf = calculatedFieldService.findById(tenantId, calculatedField.getId()); CalculatedField existingCf = calculatedFieldService.findById(tenantId, calculatedField.getId());
checkForEntityChange(existingCf, calculatedField); checkForEntityChange(existingCf, calculatedField);
} }
checkCalculatedFieldNumber(tenantId, calculatedField.getEntityId());
checkEntityExistence(tenantId, calculatedField.getEntityId()); checkEntityExistence(tenantId, calculatedField.getEntityId());
checkArgumentSize(calculatedField.getConfiguration());
checkReferencedEntities(calculatedField.getConfiguration(), user); checkReferencedEntities(calculatedField.getConfiguration(), user);
CalculatedField savedCalculatedField = checkNotNull(calculatedFieldService.save(calculatedField)); CalculatedField savedCalculatedField = checkNotNull(calculatedFieldService.save(calculatedField));
logEntityActionService.logEntityAction(tenantId, savedCalculatedField.getId(), savedCalculatedField, actionType, user); logEntityActionService.logEntityAction(tenantId, savedCalculatedField.getId(), savedCalculatedField, actionType, user);
@ -129,19 +124,6 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
} }
private void checkArgumentSize(CalculatedFieldConfiguration calculatedFieldConfig) {
if (calculatedFieldConfig.getArguments().size() > MAX_ARGUMENT_SIZE) {
throw new IllegalArgumentException("Too many arguments: " + calculatedFieldConfig.getArguments().size() + ". Max number of argument is " + MAX_ARGUMENT_SIZE);
}
}
private void checkCalculatedFieldNumber(TenantId tenantId, EntityId entityId) {
int numberOfCalculatedFieldsByEntityId = calculatedFieldService.findCalculatedFieldIdsByEntityId(tenantId, entityId).size();
if (numberOfCalculatedFieldsByEntityId >= MAX_CALCULATED_FIELD_NUMBER) {
throw new IllegalArgumentException("Max number of calculated fields for entity is " + MAX_CALCULATED_FIELD_NUMBER);
}
}
private <E extends HasId<I> & HasTenantId, I extends EntityId> E findEntity(TenantId tenantId, EntityId entityId) { private <E extends HasId<I> & HasTenantId, I extends EntityId> E findEntity(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) { return switch (entityId.getEntityType()) {
case TENANT, CUSTOMER, ASSET, DEVICE -> (E) entityService.fetchEntity(tenantId, entityId).orElse(null); case TENANT, CUSTOMER, ASSET, DEVICE -> (E) entityService.fetchEntity(tenantId, entityId).orElse(null);

View File

@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
@ -103,7 +102,6 @@ import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -729,13 +727,13 @@ public class DefaultTbClusterService implements TbClusterService {
@Override @Override
public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) { public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) {
var msg = new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getEntityId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); var msg = new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), callback); broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), callback);
} }
@Override @Override
public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) { public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) {
var msg = new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getEntityId(), ComponentLifecycleEvent.DELETED); var msg = new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED);
broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), callback); broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), callback);
} }

View File

@ -34,6 +34,8 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap; import java.util.HashMap;
@ -51,7 +53,7 @@ public class ScriptCalculatedFieldStateTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb"));
private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76")); private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76"));
private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, 43, 122L); private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("assetHumidity", 43L), 122L);
private final TsRollingArgumentEntry deviceTemperatureArgEntry = createRollingArgEntry(); private final TsRollingArgumentEntry deviceTemperatureArgEntry = createRollingArgEntry();
private final long ts = System.currentTimeMillis(); private final long ts = System.currentTimeMillis();
@ -65,6 +67,7 @@ public class ScriptCalculatedFieldStateTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService); ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService);
ctx.init();
state = new ScriptCalculatedFieldState(ctx.getArgNames()); state = new ScriptCalculatedFieldState(ctx.getArgNames());
} }
@ -93,7 +96,7 @@ public class ScriptCalculatedFieldStateTest {
void testUpdateStateWhenUpdateExistingEntry() { void testUpdateStateWhenUpdateExistingEntry() {
state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry));
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, 41, 349L); SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L);
Map<String, ArgumentEntry> newArgs = Map.of("assetHumidity", newArgEntry); Map<String, ArgumentEntry> newArgs = Map.of("assetHumidity", newArgEntry);
boolean stateUpdated = state.updateState(newArgs); boolean stateUpdated = state.updateState(newArgs);
@ -116,17 +119,17 @@ public class ScriptCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 13.0, "assetHumidity", 43)); assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 13.0, "assetHumidity", 43L));
} }
@Test @Test
void testPerformCalculationWhenOldTelemetry() throws ExecutionException, InterruptedException { void testPerformCalculationWhenOldTelemetry() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, Object> values = new TreeMap<>(); TreeMap<Long, BasicKvEntry> values = new TreeMap<>();
values.put(ts - 40000, 4);// will not be used for calculation values.put(ts - 40000, new LongDataEntry("deviceTemperature", 4L));// will not be used for calculation
values.put(ts - 45000, 2);// will not be used for calculation values.put(ts - 45000, new LongDataEntry("deviceTemperature", 2L));// will not be used for calculation
values.put(ts - 20, 0); values.put(ts - 20, new LongDataEntry("deviceTemperature", 0L));
argumentEntry.setTsRecords(values); argumentEntry.setTsRecords(values);
@ -138,19 +141,19 @@ public class ScriptCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 0.0, "assetHumidity", 43)); assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 0.0, "assetHumidity", 43L));
} }
@Test @Test
void testPerformCalculationWhenArgumentsMoreThanLimit() throws ExecutionException, InterruptedException { void testPerformCalculationWhenArgumentsMoreThanLimit() throws ExecutionException, InterruptedException {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
TreeMap<Long, Object> values = new TreeMap<>(); TreeMap<Long, BasicKvEntry> values = new TreeMap<>();
values.put(ts - 20, 1000);// will not be used values.put(ts - 20, new LongDataEntry("deviceTemperature", 1000L));// will not be used
values.put(ts - 18, 0); values.put(ts - 18, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 16, 0); values.put(ts - 16, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 14, 0); values.put(ts - 14, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 12, 0); values.put(ts - 12, new LongDataEntry("deviceTemperature", 0L));
values.put(ts - 10, 0); values.put(ts - 10, new LongDataEntry("deviceTemperature", 0L));
argumentEntry.setTsRecords(values); argumentEntry.setTsRecords(values);
state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry)); state.arguments = new HashMap<>(Map.of("deviceTemperature", argumentEntry, "assetHumidity", assetHumidityArgEntry));
@ -161,7 +164,7 @@ public class ScriptCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 0.0, "assetHumidity", 43)); assertThat(result.getResultMap()).isEqualTo(Map.of("averageDeviceTemperature", 0.0, "assetHumidity", 43L));
} }
@Test @Test
@ -187,10 +190,10 @@ public class ScriptCalculatedFieldStateTest {
TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry(); TsRollingArgumentEntry argumentEntry = new TsRollingArgumentEntry();
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
TreeMap<Long, Object> values = new TreeMap<>(); TreeMap<Long, BasicKvEntry> values = new TreeMap<>();
values.put(ts - 40, 10); values.put(ts - 40, new LongDataEntry("deviceTemperature", 10L));
values.put(ts - 30, 12); values.put(ts - 30, new LongDataEntry("deviceTemperature", 12L));
values.put(ts - 20, 17); values.put(ts - 20, new LongDataEntry("deviceTemperature", 17L));
argumentEntry.setTsRecords(values); argumentEntry.setTsRecords(values);
return argumentEntry; return argumentEntry;

View File

@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.HashMap; import java.util.HashMap;
@ -46,9 +48,9 @@ public class SimpleCalculatedFieldStateTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb"));
private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76")); private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76"));
private final SingleValueArgumentEntry key1ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, 11, 145L); private final SingleValueArgumentEntry key1ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("key1", 11L), 145L);
private final SingleValueArgumentEntry key2ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, 15, 165L); private final SingleValueArgumentEntry key2ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new LongDataEntry("key2", 15L), 165L);
private final SingleValueArgumentEntry key3ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 3, 23, 184L); private final SingleValueArgumentEntry key3ArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 3, new LongDataEntry("key3", 23L), 184L);
private SimpleCalculatedFieldState state; private SimpleCalculatedFieldState state;
private CalculatedFieldCtx ctx; private CalculatedFieldCtx ctx;
@ -56,6 +58,7 @@ public class SimpleCalculatedFieldStateTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
ctx = new CalculatedFieldCtx(getCalculatedField(), null); ctx = new CalculatedFieldCtx(getCalculatedField(), null);
ctx.init();
state = new SimpleCalculatedFieldState(ctx.getArgNames()); state = new SimpleCalculatedFieldState(ctx.getArgNames());
} }
@ -88,7 +91,7 @@ public class SimpleCalculatedFieldStateTest {
void testUpdateStateWhenUpdateExistingEntry() { void testUpdateStateWhenUpdateExistingEntry() {
state.arguments = new HashMap<>(Map.of("key1", key1ArgEntry)); state.arguments = new HashMap<>(Map.of("key1", key1ArgEntry));
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), 18, 190L); SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L);
Map<String, ArgumentEntry> newArgs = Map.of("key1", newArgEntry); Map<String, ArgumentEntry> newArgs = Map.of("key1", newArgEntry);
boolean stateUpdated = state.updateState(newArgs); boolean stateUpdated = state.updateState(newArgs);
@ -130,7 +133,7 @@ public class SimpleCalculatedFieldStateTest {
void testPerformCalculationWhenPassedNotNumber() { void testPerformCalculationWhenPassedNotNumber() {
state.arguments = new HashMap<>(Map.of( state.arguments = new HashMap<>(Map.of(
"key1", key1ArgEntry, "key1", key1ArgEntry,
"key2", new SingleValueArgumentEntry(System.currentTimeMillis() - 9, "string", 124L), "key2", new SingleValueArgumentEntry(System.currentTimeMillis() - 9, new StringDataEntry("key2", "string"), 124L),
"key3", key3ArgEntry "key3", key3ArgEntry
)); ));

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -29,7 +30,7 @@ public class SingleValueArgumentEntryTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
entry = new SingleValueArgumentEntry(ts, 11, 363L); entry = new SingleValueArgumentEntry(ts, new LongDataEntry("key", 11L), 363L);
} }
@Test @Test
@ -46,26 +47,26 @@ public class SingleValueArgumentEntryTest {
@Test @Test
void testUpdateEntryWithThaSameTs() { void testUpdateEntryWithThaSameTs() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, 13, 363L))).isFalse(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L))).isFalse();
} }
@Test @Test
void testUpdateEntryWhenNewVersionIsNull() { void testUpdateEntryWhenNewVersionIsNull() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, 13, null))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null))).isTrue();
assertThat(entry.getValue()).isEqualTo(13); assertThat(entry.getValue()).isEqualTo(13L);
assertThat(entry.getVersion()).isNull(); assertThat(entry.getVersion()).isNull();
} }
@Test @Test
void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, 18, 369L))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L))).isTrue();
assertThat(entry.getValue()).isEqualTo(18); assertThat(entry.getValue()).isEqualTo(18L);
assertThat(entry.getVersion()).isEqualTo(369L); assertThat(entry.getVersion()).isEqualTo(369L);
} }
@Test @Test
void testUpdateEntryWhenNewVersionIsLessThanCurrent() { void testUpdateEntryWhenNewVersionIsLessThanCurrent() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, 18, 234L))).isFalse(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L))).isFalse();
} }
} }

View File

@ -54,7 +54,7 @@ public class TsRollingArgumentEntryTest {
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.getTsRecords()).hasSize(4); assertThat(entry.getTsRecords()).hasSize(4);
assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23); assertThat(entry.getTsRecords().get(ts - 10).getValue()).isEqualTo(23.0);
} }
@Test @Test
@ -76,11 +76,11 @@ public class TsRollingArgumentEntryTest {
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.getTsRecords()).hasSize(5); assertThat(entry.getTsRecords()).hasSize(5);
assertThat(entry.getTsRecords()).isEqualTo(Map.of( assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 40, 10, ts - 40, new DoubleDataEntry("key", 10.0),
ts - 30, 12, ts - 30, new DoubleDataEntry("key", 12.0),
ts - 20, 17, ts - 20, new DoubleDataEntry("key", 17.0),
ts - 10, 7, ts - 10, new DoubleDataEntry("key", 7.0),
ts - 5, 1 ts - 5, new DoubleDataEntry("key", 1.0)
)); ));
} }

View File

@ -135,6 +135,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private double warnThreshold; private double warnThreshold;
private long maxCalculatedFieldsPerTenant;
private long maxCalculatedFieldsPerEntity;
private long maxArgumentsPerCF;
private long maxDataPointsPerRollingArg;
private long maxStateSizeInKBytes;
@Override @Override
public long getProfileThreshold(ApiUsageRecordKey key) { public long getProfileThreshold(ApiUsageRecordKey key) {
return switch (key) { return switch (key) {
@ -175,6 +181,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
case DASHBOARD -> maxDashboards; case DASHBOARD -> maxDashboards;
case RULE_CHAIN -> maxRuleChains; case RULE_CHAIN -> maxRuleChains;
case EDGE -> maxEdges; case EDGE -> maxEdges;
case CALCULATED_FIELD -> maxCalculatedFieldsPerTenant;
default -> 0; default -> 0;
}; };
} }

View File

@ -43,4 +43,6 @@ public interface CalculatedFieldDao extends Dao<CalculatedField> {
boolean existsByEntityId(TenantId tenantId, EntityId entityId); boolean existsByEntityId(TenantId tenantId, EntityId entityId);
long countCFByEntityId(TenantId tenantId, EntityId entityId);
} }

View File

@ -17,11 +17,15 @@ package org.thingsboard.server.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.cf.CalculatedFieldDao; import org.thingsboard.server.dao.cf.CalculatedFieldDao;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
@Component @Component
public class CalculatedFieldDataValidator extends DataValidator<CalculatedField> { public class CalculatedFieldDataValidator extends DataValidator<CalculatedField> {
@ -29,13 +33,40 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
@Autowired @Autowired
private CalculatedFieldDao calculatedFieldDao; private CalculatedFieldDao calculatedFieldDao;
@Autowired
private ApiLimitService apiLimitService;
@Override
protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) {
validateNumberOfEntitiesPerTenant(tenantId, EntityType.CALCULATED_FIELD);
validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId());
validateNumberOfArgumentsPerCF(tenantId, calculatedField);
}
@Override @Override
protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) {
CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId());
if (old == null) { if (old == null) {
throw new DataValidationException("Can't update non existing calculated field!"); throw new DataValidationException("Can't update non existing calculated field!");
} }
validateNumberOfArgumentsPerCF(tenantId, calculatedField);
return old; return old;
} }
private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) {
long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity);
long countCFByEntityId = calculatedFieldDao.countCFByEntityId(tenantId, entityId);
if (countCFByEntityId == maxCFsPerEntity) {
throw new DataValidationException("Calculated fields per entity limit reached!");
}
}
private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) {
long maxArgumentsPerCF = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxArgumentsPerCF);
if (calculatedField.getConfiguration().getArguments().size() > maxArgumentsPerCF) {
throw new DataValidationException("Calculated field arguments limit reached!");
}
}
} }

View File

@ -38,4 +38,6 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
List<CalculatedFieldEntity> removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); List<CalculatedFieldEntity> removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
long countByTenantIdAndEntityId(UUID tenantId, UUID entityId);
} }

View File

@ -88,6 +88,11 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
return calculatedFieldRepository.existsByTenantIdAndEntityId(tenantId.getId(), entityId.getId()); return calculatedFieldRepository.existsByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
} }
@Override
public long countCFByEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldRepository.countByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
}
@Override @Override
protected Class<CalculatedFieldEntity> getEntityClass() { protected Class<CalculatedFieldEntity> getEntityClass() {
return CalculatedFieldEntity.class; return CalculatedFieldEntity.class;