diff --git a/.github/workflows/check-configuration-files.yml b/.github/workflows/check-configuration-files.yml
index f280e326c2..561b7d0019 100644
--- a/.github/workflows/check-configuration-files.yml
+++ b/.github/workflows/check-configuration-files.yml
@@ -32,14 +32,14 @@ on:
jobs:
build:
name: Check thingsboard.yml file
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v2
- - name: Set up Python 3.10
+ - name: Set up Python 3.13
uses: actions/setup-python@v3
with:
- python-version: "3.10.2"
+ python-version: "3.13.2"
architecture: "x64"
env:
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache
diff --git a/application/pom.xml b/application/pom.xml
index c6058408f8..6ce77646f8 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -124,6 +124,10 @@
org.thingsboard.common
edge-api
+
+ org.thingsboard.common
+ edqs
+
org.thingsboard
dao
diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
index 9bb838963f..81f9e6a14d 100644
--- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
+++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
@@ -122,7 +122,7 @@
"type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode",
"name": "Push to cloud",
"configuration": {
- "scope": "SERVER_SCOPE"
+ "scope": "CLIENT_SCOPE"
},
"externalId": null
},
diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql
index 04dce447c5..29c7a084f4 100644
--- a/application/src/main/data/upgrade/basic/schema_update.sql
+++ b/application/src/main/data/upgrade/basic/schema_update.sql
@@ -66,3 +66,20 @@ WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode'
-- UPDATE SAVE ATTRIBUTES NODES END
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+
+-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS START
+
+UPDATE tenant_profile
+SET profile_data = profile_data
+ || jsonb_build_object(
+ 'configuration', profile_data->'configuration' || jsonb_build_object(
+ 'maxCalculatedFieldsPerEntity', COALESCE(profile_data->'configuration'->>'maxCalculatedFieldsPerEntity', '5')::bigint,
+ 'maxArgumentsPerCF', COALESCE(profile_data->'configuration'->>'maxArgumentsPerCF', '10')::bigint,
+ 'maxDataPointsPerRollingArg', COALESCE(profile_data->'configuration'->>'maxDataPointsPerRollingArg', '1000')::bigint,
+ 'maxStateSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxStateSizeInKBytes', '32')::bigint,
+ 'maxSingleValueArgumentSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxSingleValueArgumentSizeInKBytes', '2')::bigint
+ )
+ )
+WHERE profile_data->'configuration'->>'maxCalculatedFieldsPerEntity' IS NULL;
+
+-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS END
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 50a777840c..2c22941c2f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -33,7 +33,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter;
-import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager;
+import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@@ -109,6 +109,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
+import org.thingsboard.server.service.cf.CalculatedFieldQueueService;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
@@ -233,7 +234,7 @@ public class ActorSystemContext {
@Autowired(required = false)
@Getter
- private RuleEngineDeviceStateManager deviceStateManager;
+ private DeviceStateManager deviceStateManager;
@Autowired
@Getter
@@ -544,6 +545,11 @@ public class ActorSystemContext {
@Getter
private CalculatedFieldStateService calculatedFieldStateService;
+ @Lazy
+ @Autowired(required = false)
+ @Getter
+ private CalculatedFieldQueueService calculatedFieldQueueService;
+
@Lazy
@Autowired(required = false)
@Getter
@@ -644,6 +650,10 @@ public class ActorSystemContext {
@Getter
private String deviceStateNodeRateLimitConfig;
+ @Value("${actors.calculated_fields.calculation_timeout:5}")
+ @Getter
+ private long cfCalculationResultTimeout;
+
@Getter
@Setter
private TbActorSystem actorSystem;
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 f7fc204c0f..61f46b7901 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
@@ -242,7 +242,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
justRestored = true;
}
if (state.isSizeOk()) {
- if (state.updateState(newArgValues) || justRestored) {
+ if (state.updateState(ctx, newArgValues) || justRestored) {
cfIdList = new ArrayList<>(cfIdList);
cfIdList.add(ctx.getCfId());
processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback);
@@ -274,36 +274,34 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
- boolean stateSizeOk;
- if (ctx.isInitialized() && state.isReady()) {
- try {
- CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS);
+ boolean stateSizeChecked = false;
+ try {
+ if (ctx.isInitialized() && state.isReady()) {
+ CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize());
- stateSizeOk = state.isSizeOk();
- if (stateSizeOk) {
+ stateSizeChecked = true;
+ if (state.isSizeOk()) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null);
}
}
- } catch (Exception e) {
- throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
}
- } else {
- state.checkStateSize(ctxId, ctx.getMaxStateSize());
- stateSizeOk = state.isSizeOk();
- if (stateSizeOk) {
- callback.onSuccess(); // State was updated but no calculation performed;
+ } catch (Exception e) {
+ throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
+ } finally {
+ if (!stateSizeChecked) {
+ state.checkStateSize(ctxId, ctx.getMaxStateSize());
+ }
+ if (state.isSizeOk()) {
+ cfStateService.persistState(ctxId, state, callback);
+ } else {
+ removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback);
}
- }
- if (stateSizeOk) {
- cfStateService.persistState(ctxId, state, callback);
- } else {
- removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback);
}
}
- private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) {
+ private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException {
// We remove the state, but remember that it is over-sized in a local map.
cfStateService.removeState(ctxId, new TbCallback() {
@Override
@@ -316,6 +314,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
callback.onFailure(ex);
}
});
+ throw ex;
}
private Map mapToArguments(CalculatedFieldCtx ctx, List data) {
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 d26d89626e..e7c963a4a7 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
@@ -16,7 +16,6 @@
package org.thingsboard.server.actors.calculatedField;
import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef;
@@ -51,8 +50,6 @@ 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.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index d162b6a09a..ee6758df6a 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -28,8 +28,9 @@ import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
+import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
-import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager;
+import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
@@ -725,7 +726,7 @@ public class DefaultTbContext implements TbContext {
}
@Override
- public RuleEngineDeviceStateManager getDeviceStateManager() {
+ public DeviceStateManager getDeviceStateManager() {
return mainCtx.getDeviceStateManager();
}
@@ -902,6 +903,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getCalculatedFieldService();
}
+ @Override
+ public RuleEngineCalculatedFieldQueueService getCalculatedFieldQueueService() {
+ return mainCtx.getCalculatedFieldQueueService();
+ }
+
@Override
public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck();
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
index 0387e8c24a..7fd0b12077 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
@@ -20,6 +20,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@@ -38,6 +41,8 @@ import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
+import org.thingsboard.server.common.msg.edqs.EdqsApiService;
+import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.query.EntityQueryService;
@@ -55,6 +60,10 @@ public class EntityQueryController extends BaseController {
@Autowired
private EntityQueryService entityQueryService;
+ @Autowired
+ private EdqsService edqsService;
+ @Autowired
+ private EdqsApiService edqsApiService;
private static final int MAX_PAGE_SIZE = 100;
@@ -133,4 +142,16 @@ public class EntityQueryController extends BaseController {
return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope);
}
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PostMapping("/edqs/system/request")
+ public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) {
+ edqsService.processSystemRequest(request);
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @GetMapping("/edqs/enabled")
+ public boolean isEdqsApiEnabled() {
+ return edqsApiService.isEnabled();
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java
index b688993a8d..eb86220361 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java
@@ -18,13 +18,14 @@ package org.thingsboard.server.service.cf;
import com.google.common.util.concurrent.FutureCallback;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
+import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService;
import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import java.util.List;
-public interface CalculatedFieldQueueService {
+public interface CalculatedFieldQueueService extends RuleEngineCalculatedFieldQueueService {
/**
* Filter CFs based on the request entity. Push to the queue if any matching CF exist;
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java b/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java
index cb6d3f0d6b..f95227bc24 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java
@@ -15,22 +15,29 @@
*/
package org.thingsboard.server.service.cf;
+import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.rocksdb.Options;
import org.rocksdb.WriteOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
-import org.thingsboard.server.utils.TbRocksDb;
+import org.thingsboard.server.edqs.util.TbRocksDb;
@Component
@ConditionalOnExpression("'${queue.type:null}'=='in-memory'")
public class CfRocksDb extends TbRocksDb {
- public CfRocksDb(@Value("${queue.calculated_fields.rocks_db_path:${user.home}/.rocksdb/cf_states}") String path) throws Exception {
+ public CfRocksDb(@Value("${queue.calculated_fields.rocks_db_path:${user.home}/.rocksdb/cf_states}") String path) {
super(path, new Options().setCreateIfMissing(true), new WriteOptions().setSync(true));
}
+ @PostConstruct
+ @Override
+ public void init() {
+ super.init();
+ }
+
@PreDestroy
@Override
public void close() {
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
index 2282124bd5..64487d9b3e 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
@@ -117,7 +117,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
CalculatedField calculatedField = getCalculatedField(calculatedFieldId);
if (calculatedField != null) {
ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService);
- ctx.init();
calculatedFieldsCtx.put(calculatedFieldId, ctx);
log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
index f69f5cd50b..4976045b84 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
@@ -125,7 +125,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
}
return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx);
- result.updateState(argFutures.entrySet().stream()
+ result.updateState(ctx, argFutures.entrySet().stream()
.collect(Collectors.toMap(
Entry::getKey, // Keep the key as is
entry -> {
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
index 3ca0b220ff..ba4be6ace6 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
@@ -47,6 +47,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
@@ -89,6 +90,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
}
+ @Override
+ public void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback callback) {
+ pushRequestToQueue(request, null, callback);
+ }
+
@Override
public void pushRequestToQueue(AttributesSaveRequest request, List result, FutureCallback callback) {
var tenantId = request.getTenantId();
@@ -97,6 +103,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
}
+ @Override
+ public void pushRequestToQueue(AttributesSaveRequest request, FutureCallback callback) {
+ pushRequestToQueue(request, null, callback);
+ }
+
@Override
public void pushRequestToQueue(AttributesDeleteRequest request, List result, FutureCallback callback) {
var tenantId = request.getTenantId();
@@ -156,15 +167,19 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder();
CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType());
+
List entries = request.getEntries();
- List versions = result.getVersions();
+ List versions = result != null ? result.getVersions() : Collections.emptyList();
+
for (int i = 0; i < entries.size(); i++) {
- long tsVersion = versions.get(i);
- TsKvProto tsProto = toTsKvProto(entries.get(i)).toBuilder().setVersion(tsVersion).build();
- telemetryMsg.addTsData(tsProto);
+ TsKvProto.Builder tsProtoBuilder = toTsKvProto(entries.get(i)).toBuilder();
+ if (result != null) {
+ tsProtoBuilder.setVersion(versions.get(i));
+ }
+ telemetryMsg.addTsData(tsProtoBuilder.build());
}
- msg.setTelemetryMsg(telemetryMsg.build());
+ msg.setTelemetryMsg(telemetryMsg.build());
return msg.build();
}
@@ -175,9 +190,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name()));
List entries = request.getEntries();
for (int i = 0; i < entries.size(); i++) {
- long attrVersion = versions.get(i);
- AttributeValueProto attrProto = ProtoUtils.toProto(entries.get(i)).toBuilder().setVersion(attrVersion).build();
- telemetryMsg.addAttrData(attrProto);
+ AttributeValueProto.Builder attrProtoBuilder = ProtoUtils.toProto(entries.get(i)).toBuilder();
+ if (versions != null) {
+ attrProtoBuilder.setVersion(versions.get(i));
+ }
+ telemetryMsg.addAttrData(attrProtoBuilder.build());
}
msg.setTelemetryMsg(telemetryMsg.build());
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 d59a58f296..80b003b3cc 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
@@ -25,6 +25,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
+
@Data
@AllArgsConstructor
public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
@@ -43,7 +45,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
}
@Override
- public boolean updateState(Map argumentValues) {
+ public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) {
if (arguments == null) {
arguments = new HashMap<>();
}
@@ -53,6 +55,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
for (Map.Entry entry : argumentValues.entrySet()) {
String key = entry.getKey();
ArgumentEntry newEntry = entry.getValue();
+
+ checkArgumentSize(key, newEntry, ctx);
+
ArgumentEntry existingEntry = arguments.get(key);
if (existingEntry == null || newEntry.isForceResetPrevious()) {
@@ -81,6 +86,18 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
}
}
+ @Override
+ public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) {
+ if (entry instanceof TsRollingArgumentEntry) {
+ return;
+ }
+ if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
+ if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) {
+ throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation.");
+ }
+ }
+ }
+
protected abstract void validateNewEntry(ArgumentEntry newEntry);
}
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 d3b7020c25..399e0d9f84 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
@@ -71,6 +71,7 @@ public class CalculatedFieldCtx {
private long maxDataPointsPerRollingArg;
private long maxStateSize;
+ private long maxSingleValueArgumentSize;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) {
this.calculatedField = calculatedField;
@@ -104,6 +105,7 @@ public class CalculatedFieldCtx {
this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024;
+ this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
}
public void init() {
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 70587d8b1e..fc4ba513d2 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
@@ -44,7 +44,7 @@ public interface CalculatedFieldState {
void setRequiredArguments(List requiredArguments);
- boolean updateState(Map argumentValues);
+ boolean updateState(CalculatedFieldCtx ctx, Map argumentValues);
ListenableFuture performCalculation(CalculatedFieldCtx ctx);
@@ -60,4 +60,6 @@ public interface CalculatedFieldState {
void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize);
+ void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx);
+
}
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 25008a60bd..480b334ac3 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
@@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
@@ -64,7 +65,19 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
double expressionResult = expr.evaluate();
Output output = ctx.getOutput();
- return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), JacksonUtil.valueToTree(Map.of(output.getName(), expressionResult))));
+ Object result;
+ Integer decimals = output.getDecimalsByDefault();
+ if (decimals != null) {
+ if (decimals.equals(0)) {
+ result = TbUtils.toInt(expressionResult);
+ } else {
+ result = TbUtils.toFixed(expressionResult, decimals);
+ }
+ } else {
+ result = expressionResult;
+ }
+
+ return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), JacksonUtil.valueToTree(Map.of(output.getName(), result))));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
index a064a99935..bfe9eed24f 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
@@ -42,13 +42,17 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
public SingleValueArgumentEntry(TsKvProto entry) {
this.ts = entry.getTs();
- this.version = entry.getVersion();
+ if (entry.hasVersion()) {
+ this.version = entry.getVersion();
+ }
this.kvEntryValue = ProtoUtils.fromProto(entry.getKv());
}
public SingleValueArgumentEntry(AttributeValueProto entry) {
this.ts = entry.getLastUpdateTs();
- this.version = entry.getVersion();
+ if (entry.hasVersion()) {
+ this.version = entry.getVersion();
+ }
this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry);
}
@@ -100,10 +104,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
if (newVersion == null || this.version == null || newVersion > this.version) {
this.ts = singleValueEntry.getTs();
this.version = newVersion;
- BasicKvEntry newValue = singleValueEntry.getKvEntryValue();
- if (this.kvEntryValue != null && this.kvEntryValue.getValue().equals(newValue.getValue())) {
- return false;
- }
this.kvEntryValue = singleValueEntry.getKvEntryValue();
return true;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
index 1114d993a3..b5a680a072 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
@@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
for (var e : tsRecords.entrySet()) {
values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
}
- return new TbelCfTsRollingArg(limit, timeWindow, values);
+ return new TbelCfTsRollingArg(timeWindow, values);
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
index 6f9845b9a5..c62a551310 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
@@ -82,6 +82,11 @@ public class EdgeEventSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(SaveEntityEvent> event) {
+ if (Boolean.FALSE.equals(event.getBroadcastEvent())) {
+ log.trace("Ignoring event {}", event);
+ return;
+ }
+
try {
if (!isValidSaveEntityEventForEdgeProcessing(event)) {
return;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
index 9f0e184853..a970097548 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
@@ -74,6 +74,8 @@ import org.thingsboard.server.gen.edge.v1.RequestMsgType;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ResponseMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
+import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
@@ -820,6 +822,16 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg));
}
}
+ if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) {
+ for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) {
+ result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg));
+ }
+ }
+ if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) {
+ for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) {
+ result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg));
+ }
+ }
if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) {
for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) {
result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg));
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java
index ddbe4810df..77fa31c028 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java
@@ -49,8 +49,12 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor {
@Override
public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
try {
+ EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
+ if (edgeId.equals(originatorEdgeId)) {
+ return Futures.immediateFuture(null);
+ }
switch (actionType) {
case ASSIGNED_TO_CUSTOMER: {
CustomerId customerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), CustomerId.class);
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java
new file mode 100644
index 0000000000..00b9c732aa
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java
@@ -0,0 +1,82 @@
+/**
+ * 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.service.edge.rpc.processor.rule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.util.Pair;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleChainType;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
+import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
+
+import java.util.function.Function;
+
+@Slf4j
+public class BaseRuleChainProcessor extends BaseEdgeProcessor {
+
+ @Autowired
+ private DataValidator ruleChainValidator;
+
+ protected Pair saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, RuleChainType ruleChainType) {
+ boolean created = false;
+ RuleChain ruleChainFromDb = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId);
+ if (ruleChainFromDb == null) {
+ created = true;
+ }
+
+ RuleChain ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true);
+ if (ruleChain == null) {
+ throw new RuntimeException("[{" + tenantId + "}] ruleChainUpdateMsg {" + ruleChainUpdateMsg + "} cannot be converted to rule chain");
+ }
+ boolean isRoot = ruleChain.isRoot();
+ if (RuleChainType.CORE.equals(ruleChainType)) {
+ ruleChain.setRoot(false);
+ } else {
+ ruleChain.setRoot(ruleChainFromDb == null ? false : ruleChainFromDb.isRoot());
+ }
+ ruleChain.setType(ruleChainType);
+
+ ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
+ if (created) {
+ ruleChain.setId(ruleChainId);
+ }
+ edgeCtx.getRuleChainService().saveRuleChain(ruleChain, true, false);
+ return Pair.of(created, isRoot);
+ }
+
+ protected void saveOrUpdateRuleChainMetadata(TenantId tenantId, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) {
+ RuleChainMetaData ruleChainMetadata = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
+ if (ruleChainMetadata == null) {
+ throw new RuntimeException("[{" + tenantId + "}] ruleChainMetadataUpdateMsg {" + ruleChainMetadataUpdateMsg + "} cannot be converted to rule chain metadata");
+ }
+ if (!ruleChainMetadata.getNodes().isEmpty()) {
+ ruleChainMetadata.setVersion(null);
+ for (RuleNode ruleNode : ruleChainMetadata.getNodes()) {
+ ruleNode.setRuleChainId(null);
+ ruleNode.setId(null);
+ }
+ edgeCtx.getRuleChainService().saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity(), true);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
index 772300bded..06fb4c37a2 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
@@ -15,29 +15,123 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.rule;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleChainType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
-import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
+
+import java.util.UUID;
import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY;
@Slf4j
@Component
@TbCoreComponent
-public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
+public class RuleChainEdgeProcessor extends BaseRuleChainProcessor {
+
+ public ListenableFuture processRuleChainMsgFromEdge(TenantId tenantId, Edge edge, RuleChainUpdateMsg ruleChainUpdateMsg) {
+ log.trace("[{}] executing processRuleChainMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainUpdateMsg, edge.getName());
+ RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB()));
+ try {
+ edgeSynchronizationManager.getEdgeId().set(edge.getId());
+
+ switch (ruleChainUpdateMsg.getMsgType()) {
+ case ENTITY_CREATED_RPC_MESSAGE:
+ case ENTITY_UPDATED_RPC_MESSAGE:
+ return saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge);
+ case ENTITY_DELETED_RPC_MESSAGE:
+ RuleChain ruleChainToDelete = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId);
+ if (ruleChainToDelete != null) {
+ edgeCtx.getRuleChainService().unassignRuleChainFromEdge(tenantId, ruleChainId, edge.getId(), false);
+ }
+ return Futures.immediateFuture(null);
+ case UNRECOGNIZED:
+ default:
+ return handleUnsupportedMsgType(ruleChainUpdateMsg.getMsgType());
+ }
+ } catch (DataValidationException e) {
+ if (e.getMessage().contains("limit reached")) {
+ log.warn("[{}] Number of allowed rule chains violated {}", tenantId, ruleChainUpdateMsg, e);
+ return Futures.immediateFuture(null);
+ } else {
+ return Futures.immediateFailedFuture(e);
+ }
+ } finally {
+ edgeSynchronizationManager.getEdgeId().remove();
+ }
+ }
+
+ private ListenableFuture saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) {
+ try {
+ Pair resultPair = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE);
+ Boolean created = resultPair.getFirst();
+ if (created) {
+ createRelationFromEdge(tenantId, edge.getId(), ruleChainId);
+ pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId, ruleChainUpdateMsg.getEntity());
+ edgeCtx.getRuleChainService().assignRuleChainToEdge(tenantId, ruleChainId, edge.getId());
+ }
+ Boolean isRoot = resultPair.getSecond();
+ if (isRoot) {
+ edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edge.getId());
+ edgeCtx.getEdgeService().setEdgeRootRuleChain(tenantId, edge, ruleChainId);
+ }
+ } catch (Exception e) {
+ log.error("Failed to save or update rule chain", e);
+ return Futures.immediateFailedFuture(e);
+ }
+ return Futures.immediateFuture(null);
+ }
+
+ private void pushRuleChainCreatedEventToRuleEngine(TenantId tenantId, Edge edge, RuleChainId ruleChainId, String ruleChainAsString) {
+ try {
+ TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null);
+ pushEntityEventToRuleEngine(tenantId, ruleChainId, null, TbMsgType.ENTITY_CREATED, ruleChainAsString, msgMetaData);
+ } catch (Exception e) {
+ log.warn("[{}][{}] Failed to push rule chain action to rule engine: {}", tenantId, ruleChainId, TbMsgType.ENTITY_CREATED.name(), e);
+ }
+ }
+
+ public ListenableFuture processRuleChainMetadataMsgFromEdge(TenantId tenantId, Edge edge, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) {
+ log.trace("[{}] executing processRuleChainMetadataMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainMetadataUpdateMsg, edge.getName());
+ try {
+ edgeSynchronizationManager.getEdgeId().set(edge.getId());
+
+ switch (ruleChainMetadataUpdateMsg.getMsgType()) {
+ case ENTITY_CREATED_RPC_MESSAGE:
+ case ENTITY_UPDATED_RPC_MESSAGE:
+ saveOrUpdateRuleChainMetadata(tenantId, ruleChainMetadataUpdateMsg);
+ return Futures.immediateFuture(null);
+ case UNRECOGNIZED:
+ default:
+ return handleUnsupportedMsgType(ruleChainMetadataUpdateMsg.getMsgType());
+ }
+ } catch (Exception e) {
+ String errMsg = String.format("Can't process rule chain metadata update msg %s", ruleChainMetadataUpdateMsg);
+ log.error(errMsg, e);
+ return Futures.immediateFailedFuture(new RuntimeException(errMsg, e));
+ } finally {
+ edgeSynchronizationManager.getEdgeId().remove();
+ }
+ }
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
index a371582409..6fc6d5bad9 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
@@ -152,8 +152,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
entityData = new HashMap<>();
attributes = JacksonUtil.newObjectNode();
for (AttributeKvEntry attr : ssAttributes) {
- if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())
- && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
+ if (DefaultDeviceStateService.ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT.contains(attr.getKey())) {
continue;
}
if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) {
@@ -200,7 +199,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
}
Map> tsData = new HashMap<>();
for (TsKvEntry tsKvEntry : tsKvEntries) {
- if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(tsKvEntry.getKey())) {
+ if (DefaultDeviceStateService.ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT.contains(tsKvEntry.getKey())) {
continue;
}
tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue());
diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java
new file mode 100644
index 0000000000..51c963ed2f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java
@@ -0,0 +1,117 @@
+/**
+ * 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.service.edqs;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.edqs.query.EdqsRequest;
+import org.thingsboard.server.common.data.edqs.query.EdqsResponse;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.edqs.EdqsApiService;
+import org.thingsboard.server.edqs.state.EdqsPartitionService;
+import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
+import org.thingsboard.server.queue.TbQueueRequestTemplate;
+import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.provider.EdqsClientQueueFactory;
+
+import java.util.UUID;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+@ConditionalOnExpression("'${queue.edqs.api.supported:true}' == 'true' && ('${service.type:null}' == 'monolith' || '${service.type:null}' == 'tb-core')")
+public class DefaultEdqsApiService implements EdqsApiService {
+
+ private final EdqsPartitionService edqsPartitionService;
+ private final EdqsClientQueueFactory queueFactory;
+ private TbQueueRequestTemplate, TbProtoQueueMsg> requestTemplate;
+
+ @Value("${queue.edqs.api.auto_enable:true}")
+ private boolean autoEnable;
+
+ private Boolean apiEnabled = null;
+
+ @PostConstruct
+ private void init() {
+ requestTemplate = queueFactory.createEdqsRequestTemplate();
+ requestTemplate.init();
+ }
+
+ @Override
+ public ListenableFuture processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) {
+ var requestMsg = ToEdqsMsg.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setTs(System.currentTimeMillis())
+ .setRequestMsg(TransportProtos.EdqsRequestMsg.newBuilder()
+ .setValue(JacksonUtil.toString(request))
+ .build());
+ if (customerId != null && !customerId.isNullUid()) {
+ requestMsg.setCustomerIdMSB(customerId.getId().getMostSignificantBits());
+ requestMsg.setCustomerIdLSB(customerId.getId().getLeastSignificantBits());
+ }
+
+ Integer partition = edqsPartitionService.resolvePartition(tenantId);
+ ListenableFuture> resultFuture = requestTemplate.send(new TbProtoQueueMsg<>(UUID.randomUUID(), requestMsg.build()), partition);
+ return Futures.transform(resultFuture, msg -> {
+ TransportProtos.EdqsResponseMsg responseMsg = msg.getValue().getResponseMsg();
+ return JacksonUtil.fromString(responseMsg.getValue(), EdqsResponse.class);
+ }, MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return Boolean.TRUE.equals(apiEnabled);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled) {
+ log.info("Enabling EDQS API");
+ } else {
+ log.info("Disabling EDQS API");
+ }
+ apiEnabled = enabled;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public boolean isAutoEnable() {
+ return autoEnable;
+ }
+
+ @PreDestroy
+ private void stop() {
+ requestTemplate.stop();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java
new file mode 100644
index 0000000000..e823dee4e7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java
@@ -0,0 +1,298 @@
+/**
+ * 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.service.edqs;
+
+import com.google.protobuf.ByteString;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.common.util.ThingsBoardExecutors;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.AttributeScope;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ObjectType;
+import org.thingsboard.server.common.data.edqs.EdqsEventType;
+import org.thingsboard.server.common.data.edqs.EdqsObject;
+import org.thingsboard.server.common.data.edqs.EdqsSyncRequest;
+import org.thingsboard.server.common.data.edqs.Entity;
+import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg;
+import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.JsonDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.edqs.EdqsApiService;
+import org.thingsboard.server.common.msg.edqs.EdqsService;
+import org.thingsboard.server.common.msg.queue.ServiceType;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.edqs.processor.EdqsProducer;
+import org.thingsboard.server.edqs.state.EdqsPartitionService;
+import org.thingsboard.server.edqs.util.EdqsConverter;
+import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsCoreServiceMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
+import org.thingsboard.server.queue.discovery.HashPartitionService;
+import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
+import org.thingsboard.server.queue.discovery.TopicService;
+import org.thingsboard.server.queue.edqs.EdqsQueue;
+import org.thingsboard.server.queue.environment.DistributedLock;
+import org.thingsboard.server.queue.environment.DistributedLockService;
+import org.thingsboard.server.queue.provider.EdqsClientQueueFactory;
+import org.thingsboard.server.queue.util.AfterStartUp;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "true")
+public class DefaultEdqsService implements EdqsService {
+
+ private final EdqsClientQueueFactory queueFactory;
+ private final EdqsConverter edqsConverter;
+ private final EdqsSyncService edqsSyncService;
+ private final EdqsApiService edqsApiService;
+ private final DistributedLockService distributedLockService;
+ private final AttributesService attributesService;
+ private final EdqsPartitionService edqsPartitionService;
+ private final TopicService topicService;
+ private final TbServiceInfoProvider serviceInfoProvider;
+ @Autowired @Lazy
+ private TbClusterService clusterService;
+ @Autowired @Lazy
+ private HashPartitionService hashPartitionService;
+
+ private EdqsProducer eventsProducer;
+ private ExecutorService executor;
+ private DistributedLock syncLock;
+
+ @PostConstruct
+ private void init() {
+ executor = ThingsBoardExecutors.newWorkStealingPool(12, getClass());
+ eventsProducer = EdqsProducer.builder()
+ .queue(EdqsQueue.EVENTS)
+ .partitionService(edqsPartitionService)
+ .topicService(topicService)
+ .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS))
+ .build();
+ syncLock = distributedLockService.getLock("edqs_sync");
+ }
+
+ @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
+ public void onStartUp() {
+ if (!serviceInfoProvider.isService(ServiceType.TB_CORE)) {
+ return;
+ }
+ executor.submit(() -> {
+ try {
+ EdqsSyncState syncState = getSyncState();
+ if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) {
+ if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) {
+ processSystemRequest(ToCoreEdqsRequest.builder()
+ .syncRequest(new EdqsSyncRequest())
+ .build());
+ }
+ } else if (edqsApiService.isSupported() && edqsApiService.isAutoEnable()) {
+ // only if topic/RocksDB is not empty and sync is finished
+ edqsApiService.setEnabled(true);
+ }
+ } catch (Throwable e) {
+ log.error("Failed to start EDQS service", e);
+ }
+ });
+ }
+
+ @Override
+ public void processSystemRequest(ToCoreEdqsRequest request) {
+ log.info("Processing system request {}", request);
+ if (request.getSyncRequest() != null) {
+ saveSyncState(EdqsSyncStatus.REQUESTED);
+ }
+ broadcast(request.toInternalMsg());
+ }
+
+ @Override
+ public void processSystemMsg(ToCoreEdqsMsg msg) {
+ executor.submit(() -> {
+ log.info("Processing system msg {}", msg);
+ try {
+ if (msg.getApiEnabled() != null) {
+ edqsApiService.setEnabled(msg.getApiEnabled());
+ }
+
+ if (msg.getSyncRequest() != null) {
+ syncLock.lock();
+ try {
+ EdqsSyncState syncState = getSyncState();
+ if (syncState != null && syncState.getStatus() == EdqsSyncStatus.FINISHED) {
+ log.info("EDQS sync is already finished");
+ return;
+ }
+
+ saveSyncState(EdqsSyncStatus.STARTED);
+ edqsSyncService.sync();
+ saveSyncState(EdqsSyncStatus.FINISHED);
+
+ if (edqsApiService.isSupported())
+ if (edqsApiService.isAutoEnable()) {
+ log.info("EDQS sync is finished, auto-enabling API");
+ broadcast(ToCoreEdqsMsg.builder()
+ .apiEnabled(Boolean.TRUE)
+ .build());
+ } else {
+ log.info("EDQS sync is finished, but leaving API disabled");
+ }
+ } catch (Exception e) {
+ log.error("Failed to complete sync", e);
+ saveSyncState(EdqsSyncStatus.FAILED);
+ } finally {
+ syncLock.unlock();
+ }
+ }
+ } catch (Throwable e) {
+ log.error("Failed to process msg {}", msg, e);
+ }
+ });
+ }
+
+ @Override
+ public void onUpdate(TenantId tenantId, EntityId entityId, Object entity) {
+ EntityType entityType = entityId.getEntityType();
+ ObjectType objectType = ObjectType.fromEntityType(entityType);
+ if (!isEdqsType(tenantId, objectType)) {
+ log.trace("[{}][{}] Ignoring update event, type {} not supported", tenantId, entityId, entityType);
+ return;
+ }
+ onUpdate(tenantId, objectType, edqsConverter.toEntity(entityType, entity));
+ }
+
+ @Override
+ public void onUpdate(TenantId tenantId, ObjectType objectType, EdqsObject object) {
+ processEvent(tenantId, objectType, EdqsEventType.UPDATED, object);
+ }
+
+ @Override
+ public void onDelete(TenantId tenantId, EntityId entityId) {
+ EntityType entityType = entityId.getEntityType();
+ ObjectType objectType = ObjectType.fromEntityType(entityType);
+ if (!isEdqsType(tenantId, objectType)) {
+ log.trace("[{}][{}] Ignoring deletion event, type {} not supported", tenantId, entityId, entityType);
+ return;
+ }
+ onDelete(tenantId, objectType, new Entity(entityType, entityId.getId(), Long.MAX_VALUE));
+ }
+
+ @Override
+ public void onDelete(TenantId tenantId, ObjectType objectType, EdqsObject object) {
+ processEvent(tenantId, objectType, EdqsEventType.DELETED, object);
+ }
+
+ protected void processEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType, EdqsObject object) {
+ executor.submit(() -> {
+ try {
+ String key = object.key();
+ Long version = object.version();
+ EdqsEventMsg.Builder eventMsg = EdqsEventMsg.newBuilder()
+ .setKey(key)
+ .setObjectType(objectType.name())
+ .setData(ByteString.copyFrom(edqsConverter.serialize(objectType, object)))
+ .setEventType(eventType.name());
+ if (version != null) {
+ eventMsg.setVersion(version);
+ }
+ eventsProducer.send(tenantId, objectType, key, ToEdqsMsg.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setTs(System.currentTimeMillis())
+ .setEventMsg(eventMsg)
+ .build());
+ } catch (Throwable e) {
+ log.error("[{}] Failed to push {} event for {} {}", tenantId, eventType, objectType, object, e);
+ }
+ });
+ }
+
+ private boolean isEdqsType(TenantId tenantId, ObjectType objectType) {
+ if (objectType == null) {
+ return false;
+ }
+ if (!tenantId.isSysTenantId()) {
+ return ObjectType.edqsTypes.contains(objectType);
+ } else {
+ return ObjectType.edqsSystemTypes.contains(objectType);
+ }
+ }
+
+ private void broadcast(ToCoreEdqsMsg msg) {
+ clusterService.broadcastToCore(ToCoreNotificationMsg.newBuilder()
+ .setToEdqsCoreServiceMsg(ToEdqsCoreServiceMsg.newBuilder()
+ .setValue(ByteString.copyFrom(JacksonUtil.writeValueAsBytes(msg))))
+ .build());
+ }
+
+ @SneakyThrows
+ private EdqsSyncState getSyncState() {
+ EdqsSyncState state = attributesService.find(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, "edqsSyncState").get(30, TimeUnit.SECONDS)
+ .flatMap(KvEntry::getJsonValue)
+ .map(value -> JacksonUtil.fromString(value, EdqsSyncState.class))
+ .orElse(null);
+ log.info("EDQS sync state: {}", state);
+ return state;
+ }
+
+ @SneakyThrows
+ private void saveSyncState(EdqsSyncStatus status) {
+ EdqsSyncState state = new EdqsSyncState(status);
+ log.info("New EDQS sync state: {}", state);
+ attributesService.save(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry(
+ new JsonDataEntry("edqsSyncState", JacksonUtil.toString(state)),
+ System.currentTimeMillis())).get(30, TimeUnit.SECONDS);
+ }
+
+ @PreDestroy
+ private void stop() {
+ executor.shutdown();
+ eventsProducer.stop();
+ }
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ private static class EdqsSyncState {
+ private EdqsSyncStatus status;
+ }
+
+ private enum EdqsSyncStatus {
+ REQUESTED,
+ STARTED,
+ FINISHED,
+ FAILED
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java
new file mode 100644
index 0000000000..d77df5ced8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java
@@ -0,0 +1,61 @@
+/**
+ * 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.service.edqs;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.event.TransactionalEventListener;
+import org.thingsboard.server.common.data.ObjectType;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.msg.edqs.EdqsService;
+import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
+import org.thingsboard.server.dao.eventsourcing.RelationActionEvent;
+import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
+
+@Service
+@RequiredArgsConstructor
+@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "true")
+public class EdqsListener {
+
+ private final EdqsService edqsService;
+
+ @TransactionalEventListener(fallbackExecution = true)
+ public void onUpdate(SaveEntityEvent> event) {
+ if (event.getEntityId() == null || event.getEntity() == null) {
+ return;
+ }
+ edqsService.onUpdate(event.getTenantId(), event.getEntityId(), event.getEntity());
+ }
+
+ @TransactionalEventListener(fallbackExecution = true)
+ public void onDelete(DeleteEntityEvent> event) {
+ if (event.getEntityId() == null) {
+ return;
+ }
+ edqsService.onDelete(event.getTenantId(), event.getEntityId());
+ }
+
+ @TransactionalEventListener(fallbackExecution = true)
+ public void handleEvent(RelationActionEvent relationEvent) {
+ if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) {
+ edqsService.onUpdate(relationEvent.getTenantId(), ObjectType.RELATION, relationEvent.getRelation());
+ } else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) {
+ edqsService.onDelete(relationEvent.getTenantId(), ObjectType.RELATION, relationEvent.getRelation());
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java
new file mode 100644
index 0000000000..79e0e60983
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java
@@ -0,0 +1,284 @@
+/**
+ * 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.service.edqs;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.thingsboard.server.common.data.AttributeScope;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ObjectType;
+import org.thingsboard.server.common.data.edqs.AttributeKv;
+import org.thingsboard.server.common.data.edqs.EdqsEventType;
+import org.thingsboard.server.common.data.edqs.EdqsObject;
+import org.thingsboard.server.common.data.edqs.Entity;
+import org.thingsboard.server.common.data.edqs.LatestTsKv;
+import org.thingsboard.server.common.data.edqs.fields.EntityFields;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.attributes.AttributesDao;
+import org.thingsboard.server.dao.dictionary.KeyDictionaryDao;
+import org.thingsboard.server.dao.entity.EntityDaoRegistry;
+import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
+import org.thingsboard.server.dao.model.sql.RelationEntity;
+import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
+import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
+import org.thingsboard.server.dao.sql.relation.RelationRepository;
+import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV;
+import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV;
+import static org.thingsboard.server.common.data.ObjectType.RELATION;
+import static org.thingsboard.server.common.data.ObjectType.edqsTenantTypes;
+
+@Slf4j
+public abstract class EdqsSyncService {
+
+ @Value("${queue.edqs.sync.entity_batch_size:10000}")
+ private int entityBatchSize;
+ @Value("${queue.edqs.sync.ts_batch_size:10000}")
+ private int tsBatchSize;
+ @Autowired
+ private EntityDaoRegistry entityDaoRegistry;
+ @Autowired
+ private AttributesDao attributesDao;
+ @Autowired
+ private KeyDictionaryDao keyDictionaryDao;
+ @Autowired
+ private RelationRepository relationRepository;
+ @Autowired
+ private TsKvLatestRepository tsKvLatestRepository;
+ @Autowired
+ @Lazy
+ private DefaultEdqsService edqsService;
+
+ private final ConcurrentHashMap entityInfoMap = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap keys = new ConcurrentHashMap<>();
+
+ private final Map counters = new ConcurrentHashMap<>();
+
+ public abstract boolean isSyncNeeded();
+
+ public void sync() {
+ log.info("Synchronizing data to EDQS");
+ long startTs = System.currentTimeMillis();
+ counters.clear();
+
+ syncTenantEntities();
+ syncRelations();
+ loadKeyDictionary();
+ syncAttributes();
+ syncLatestTimeseries();
+
+ counters.clear();
+ log.info("Finishing synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs));
+ }
+
+ private void process(TenantId tenantId, ObjectType type, EdqsObject object) {
+ AtomicInteger counter = counters.computeIfAbsent(type, t -> new AtomicInteger());
+ if (counter.incrementAndGet() % 10000 == 0) {
+ log.info("Processed {} {} objects", counter.get(), type);
+ }
+ edqsService.processEvent(tenantId, type, EdqsEventType.UPDATED, object);
+ }
+
+ private void syncTenantEntities() {
+ for (ObjectType type : edqsTenantTypes) {
+ log.info("Synchronizing {} entities to EDQS", type);
+ long ts = System.currentTimeMillis();
+ EntityType entityType = type.toEntityType();
+ Dao> dao = entityDaoRegistry.getDao(entityType);
+ UUID lastId = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ while (true) {
+ var batch = dao.findNextBatch(lastId, entityBatchSize);
+ if (batch.isEmpty()) {
+ break;
+ }
+ for (EntityFields entityFields : batch) {
+ TenantId tenantId = TenantId.fromUUID(entityFields.getTenantId());
+ entityInfoMap.put(entityFields.getId(), new EntityIdInfo(entityType, tenantId));
+ process(tenantId, type, new Entity(entityType, entityFields));
+ }
+ EntityFields lastRecord = batch.get(batch.size() - 1);
+ lastId = lastRecord.getId();
+ }
+ log.info("Finished synchronizing {} entities to EDQS in {} ms", type, (System.currentTimeMillis() - ts));
+ }
+ }
+
+ private void syncRelations() {
+ log.info("Synchronizing relations to EDQS");
+ long ts = System.currentTimeMillis();
+ UUID lastFromEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ String lastFromEntityType = "";
+ String lastRelationTypeGroup = "";
+ String lastRelationType = "";
+ UUID lastToEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ String lastToEntityType = "";
+
+ while (true) {
+ List batch = relationRepository.findNextBatch(lastFromEntityId, lastFromEntityType, lastRelationTypeGroup,
+ lastRelationType, lastToEntityId, lastToEntityType, entityBatchSize);
+ if (batch.isEmpty()) {
+ break;
+ }
+ processRelationBatch(batch);
+
+ RelationEntity lastRecord = batch.get(batch.size() - 1);
+ lastFromEntityId = lastRecord.getFromId();
+ lastFromEntityType = lastRecord.getFromType();
+ lastRelationTypeGroup = lastRecord.getRelationTypeGroup();
+ lastRelationType = lastRecord.getRelationType();
+ lastToEntityId = lastRecord.getToId();
+ lastToEntityType = lastRecord.getToType();
+ }
+ log.info("Finished synchronizing relations to EDQS in {} ms", (System.currentTimeMillis() - ts));
+ }
+
+ private void processRelationBatch(List relations) {
+ for (RelationEntity relation : relations) {
+ if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup())) {
+ EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId());
+ if (entityIdInfo != null) {
+ process(entityIdInfo.tenantId(), RELATION, relation.toData());
+ } else {
+ log.info("Relation from id not found: {} ", relation);
+ }
+ }
+ }
+ }
+
+ private void loadKeyDictionary() {
+ log.info("Loading key dictionary");
+ long ts = System.currentTimeMillis();
+ var keyDictionaryEntries = new PageDataIterable<>(keyDictionaryDao::findAll, 10000);
+ for (KeyDictionaryEntry keyDictionaryEntry : keyDictionaryEntries) {
+ keys.put(keyDictionaryEntry.getKeyId(), keyDictionaryEntry.getKey());
+ }
+ log.info("Finished loading key dictionary in {} ms", (System.currentTimeMillis() - ts));
+ }
+
+ private void syncAttributes() {
+ log.info("Synchronizing attributes to EDQS");
+ long ts = System.currentTimeMillis();
+
+ UUID lastEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ int lastAttributeType = Integer.MIN_VALUE;
+ int lastAttributeKey = Integer.MIN_VALUE;
+
+ while (true) {
+ List batch = attributesDao.findNextBatch(lastEntityId, lastAttributeType, lastAttributeKey, tsBatchSize);
+ if (batch.isEmpty()) {
+ break;
+ }
+ processAttributeBatch(batch);
+
+ AttributeKvEntity lastRecord = batch.get(batch.size() - 1);
+ lastEntityId = lastRecord.getId().getEntityId();
+ lastAttributeType = lastRecord.getId().getAttributeType();
+ lastAttributeKey = lastRecord.getId().getAttributeKey();
+ }
+ log.info("Finished synchronizing attributes to EDQS in {} ms", (System.currentTimeMillis() - ts));
+ }
+
+ private void processAttributeBatch(List batch) {
+ for (AttributeKvEntity attribute : batch) {
+ attribute.setStrKey(getStrKeyOrFetchFromDb(attribute.getId().getAttributeKey()));
+ UUID entityId = attribute.getId().getEntityId();
+ EntityIdInfo entityIdInfo = entityInfoMap.get(entityId);
+ if (entityIdInfo == null) {
+ log.debug("Skipping attribute with entity UUID {} as it is not found in entityInfoMap", entityId);
+ continue;
+ }
+ AttributeKv attributeKv = new AttributeKv(
+ EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityId),
+ AttributeScope.valueOf(attribute.getId().getAttributeType()),
+ attribute.toData(),
+ attribute.getVersion());
+ process(entityIdInfo.tenantId(), ATTRIBUTE_KV, attributeKv);
+ }
+ }
+
+ private void syncLatestTimeseries() {
+ log.info("Synchronizing latest timeseries to EDQS");
+ long ts = System.currentTimeMillis();
+ UUID lastEntityId = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ int lastKey = Integer.MIN_VALUE;
+
+ while (true) {
+ List batch = tsKvLatestRepository.findNextBatch(lastEntityId, lastKey, tsBatchSize);
+ if (batch.isEmpty()) {
+ break;
+ }
+ processTsKvLatestBatch(batch);
+
+ TsKvLatestEntity lastRecord = batch.get(batch.size() - 1);
+ lastEntityId = lastRecord.getEntityId();
+ lastKey = lastRecord.getKey();
+ }
+ log.info("Finished synchronizing latest timeseries to EDQS in {} ms", (System.currentTimeMillis() - ts));
+ }
+
+ private void processTsKvLatestBatch(List tsKvLatestEntities) {
+ for (TsKvLatestEntity tsKvLatestEntity : tsKvLatestEntities) {
+ try {
+ String strKey = getStrKeyOrFetchFromDb(tsKvLatestEntity.getKey());
+ if (strKey == null) {
+ log.debug("Skipping latest timeseries with key {} as it is not found in key dictionary", tsKvLatestEntity.getKey());
+ continue;
+ }
+ tsKvLatestEntity.setStrKey(strKey);
+ UUID entityUuid = tsKvLatestEntity.getEntityId();
+ EntityIdInfo entityIdInfo = entityInfoMap.get(entityUuid);
+ if (entityIdInfo != null) {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityUuid);
+ LatestTsKv latestTsKv = new LatestTsKv(entityId, tsKvLatestEntity.toData(), tsKvLatestEntity.getVersion());
+ process(entityIdInfo.tenantId(), LATEST_TS_KV, latestTsKv);
+ }
+ } catch (Exception e) {
+ log.error("Failed to sync latest timeseries: {}", tsKvLatestEntity, e);
+ }
+ }
+ }
+
+ private String getStrKeyOrFetchFromDb(int key) {
+ String strKey = keys.get(key);
+ if (strKey != null) {
+ return strKey;
+ } else {
+ strKey = keyDictionaryDao.getKey(key);
+ if (strKey != null) {
+ keys.put(key, strKey);
+ }
+ }
+ return strKey;
+ }
+
+ public record EntityIdInfo(EntityType entityType, TenantId tenantId) {
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java
new file mode 100644
index 0000000000..4ef552521b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java
@@ -0,0 +1,42 @@
+/**
+ * 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.service.edqs;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.queue.edqs.EdqsQueue;
+import org.thingsboard.server.queue.kafka.TbKafkaAdmin;
+import org.thingsboard.server.queue.kafka.TbKafkaSettings;
+
+import java.util.Collections;
+
+@Service
+@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'")
+public class KafkaEdqsSyncService extends EdqsSyncService {
+
+ private final boolean syncNeeded;
+
+ public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings) {
+ TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap());
+ this.syncNeeded = kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic());
+ }
+
+ @Override
+ public boolean isSyncNeeded() {
+ return syncNeeded;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java
new file mode 100644
index 0000000000..904391f172
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java
@@ -0,0 +1,35 @@
+/**
+ * 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.service.edqs;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.edqs.util.EdqsRocksDb;
+
+@Service
+@RequiredArgsConstructor
+@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'in-memory'")
+public class LocalEdqsSyncService extends EdqsSyncService {
+
+ private final EdqsRocksDb db;
+
+ @Override
+ public boolean isSyncNeeded() {
+ return db.isNew();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
index 9c9ceb2b34..648e89adc9 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
@@ -52,6 +52,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
+import org.thingsboard.server.dao.edge.EdgeSynchronizationManager;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
@@ -67,6 +68,7 @@ public class EntityStateSourcingListener {
private final TenantService tenantService;
private final TbClusterService tbClusterService;
+ private final EdgeSynchronizationManager edgeSynchronizationManager;
@PostConstruct
public void init() {
@@ -75,6 +77,11 @@ public class EntityStateSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(SaveEntityEvent> event) {
+ if (Boolean.FALSE.equals(event.getBroadcastEvent())) {
+ log.trace("Ignoring event {}", event);
+ return;
+ }
+
TenantId tenantId = event.getTenantId();
EntityId entityId = event.getEntityId();
if (entityId == null) {
@@ -270,6 +277,9 @@ public class EntityStateSourcingListener {
private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) {
if (entity instanceof Edge) {
+ if (entityId.equals(edgeSynchronizationManager.getEdgeId().get())) {
+ return;
+ }
tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent));
} else if (entity instanceof EdgeEvent edgeEvent) {
tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId()));
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index 4f0522e62a..a3003ba6ff 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
-import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
@@ -33,6 +32,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.JavaSerDesUtil;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg;
import org.thingsboard.server.common.data.event.ErrorEvent;
import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.event.LifecycleEvent;
@@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
@@ -76,6 +77,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
@@ -88,7 +90,6 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
-import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.resource.TbImageService;
@@ -146,9 +147,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig> mainConsumer;
+ private MainQueueConsumerManager, QueueConfig> mainConsumer;
private QueueConsumerManager> usageStatsConsumer;
private QueueConsumerManager> firmwareStatesConsumer;
@@ -175,7 +177,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder()
+ this.mainConsumer = MainQueueConsumerManager., QueueConfig>builder()
.queueKey(new QueueKey(ServiceType.TB_CORE))
- .config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval))
+ .config(QueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createToCoreMsgConsumer())
.consumerExecutor(consumersExecutor)
@@ -251,7 +255,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception {
+ private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig config) throws Exception {
List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
@@ -289,6 +293,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, deviceInactivityTimeoutUpdateMsg.getInactivityTimeout()));
+ DonAsynchron.withCallback(future,
+ __ -> callback.onSuccess(),
+ t -> {
+ log.warn("[{}] Failed to process device inactivity timeout update message for device [{}]", tenantId.getId(), deviceId.getId(), t);
+ callback.onFailure(t);
+ });
+ }
+
private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {
TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB());
NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()));
@@ -732,10 +757,4 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, EdgeQueueConfig> mainConsumer;
+ private MainQueueConsumerManager, QueueConfig> mainConsumer;
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
@@ -100,9 +100,9 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService, EdgeQueueConfig>builder()
+ this.mainConsumer = MainQueueConsumerManager., QueueConfig>builder()
.queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME))
- .config(EdgeQueueConfig.of(consumerPerPartition, pollInterval))
+ .config(QueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer())
.consumerExecutor(consumersExecutor)
@@ -128,7 +128,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException {
+ private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig edgeQueueConfig) throws InterruptedException {
List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
@@ -285,10 +285,4 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService {
+ event.getNewPartitions().forEach((queueKey, partitions) -> {
if (CollectionsUtil.isOneOf(queueKey, QueueKey.CF, QueueKey.CF_STATES)) {
return;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java
index 728f18bfff..46a42284b4 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java
@@ -40,6 +40,7 @@ public class TbCoreConsumerStats {
public static final String DEVICE_ACTIVITIES = "deviceActivity";
public static final String DEVICE_DISCONNECTS = "deviceDisconnect";
public static final String DEVICE_INACTIVITIES = "deviceInactivity";
+ public static final String DEVICE_INACTIVITY_TIMEOUT_UPDATES = "deviceInactivityTimeoutUpdate";
public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine
public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl";
@@ -65,6 +66,7 @@ public class TbCoreConsumerStats {
private final StatsCounter deviceActivitiesCounter;
private final StatsCounter deviceDisconnectsCounter;
private final StatsCounter deviceInactivitiesCounter;
+ private final StatsCounter deviceInactivityTimeoutUpdatesCounter;
private final StatsCounter toCoreNfOtherCounter;
private final StatsCounter toCoreNfComponentLifecycleCounter;
@@ -95,6 +97,7 @@ public class TbCoreConsumerStats {
this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES));
this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS));
this.deviceInactivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITIES));
+ this.deviceInactivityTimeoutUpdatesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITY_TIMEOUT_UPDATES));
// Core notification counters
this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER));
@@ -163,6 +166,11 @@ public class TbCoreConsumerStats {
deviceInactivitiesCounter.increment();
}
+ public void log(TransportProtos.DeviceInactivityTimeoutUpdateProto msg) {
+ totalCounter.increment();
+ deviceInactivityTimeoutUpdatesCounter.increment();
+ }
+
public void log(TransportProtos.SubscriptionMgrMsgProto msg) {
totalCounter.increment();
subscriptionMsgCounter.increment();
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
index 31813803c3..c7f7f600a7 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
@@ -72,7 +72,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager deviceStateService;
+ private final TbClusterService clusterService;
+
+ @Override
+ public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) {
+ forwardToDeviceStateService(tenantId, deviceId,
+ deviceStateService -> {
+ log.debug("[{}][{}] Forwarding device connect event to local service. Connect time: [{}].", tenantId.getId(), deviceId.getId(), connectTime);
+ deviceStateService.onDeviceConnect(tenantId, deviceId, connectTime);
+ },
+ () -> {
+ log.debug("[{}][{}] Sending device connect message to core. Connect time: [{}].", tenantId.getId(), deviceId.getId(), connectTime);
+ var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
+ .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
+ .setLastConnectTime(connectTime)
+ .build();
+ return TransportProtos.ToCoreMsg.newBuilder()
+ .setDeviceConnectMsg(deviceConnectMsg)
+ .build();
+ }, callback);
+ }
+
+ @Override
+ public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) {
+ forwardToDeviceStateService(tenantId, deviceId,
+ deviceStateService -> {
+ log.debug("[{}][{}] Forwarding device activity event to local service. Activity time: [{}].", tenantId.getId(), deviceId.getId(), activityTime);
+ deviceStateService.onDeviceActivity(tenantId, deviceId, activityTime);
+ },
+ () -> {
+ log.debug("[{}][{}] Sending device activity message to core. Activity time: [{}].", tenantId.getId(), deviceId.getId(), activityTime);
+ var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
+ .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
+ .setLastActivityTime(activityTime)
+ .build();
+ return TransportProtos.ToCoreMsg.newBuilder()
+ .setDeviceActivityMsg(deviceActivityMsg)
+ .build();
+ }, callback);
+ }
+
+ @Override
+ public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) {
+ forwardToDeviceStateService(tenantId, deviceId,
+ deviceStateService -> {
+ log.debug("[{}][{}] Forwarding device disconnect event to local service. Disconnect time: [{}].", tenantId.getId(), deviceId.getId(), disconnectTime);
+ deviceStateService.onDeviceDisconnect(tenantId, deviceId, disconnectTime);
+ },
+ () -> {
+ log.debug("[{}][{}] Sending device disconnect message to core. Disconnect time: [{}].", tenantId.getId(), deviceId.getId(), disconnectTime);
+ var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
+ .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
+ .setLastDisconnectTime(disconnectTime)
+ .build();
+ return TransportProtos.ToCoreMsg.newBuilder()
+ .setDeviceDisconnectMsg(deviceDisconnectMsg)
+ .build();
+ }, callback);
+ }
+
+ @Override
+ public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) {
+ forwardToDeviceStateService(tenantId, deviceId,
+ deviceStateService -> {
+ log.debug("[{}][{}] Forwarding device inactivity event to local service. Inactivity time: [{}].", tenantId.getId(), deviceId.getId(), inactivityTime);
+ deviceStateService.onDeviceInactivity(tenantId, deviceId, inactivityTime);
+ },
+ () -> {
+ log.debug("[{}][{}] Sending device inactivity message to core. Inactivity time: [{}].", tenantId.getId(), deviceId.getId(), inactivityTime);
+ var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
+ .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
+ .setLastInactivityTime(inactivityTime)
+ .build();
+ return TransportProtos.ToCoreMsg.newBuilder()
+ .setDeviceInactivityMsg(deviceInactivityMsg)
+ .build();
+ }, callback);
+ }
+
+ @Override
+ public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout, TbCallback callback) {
+ forwardToDeviceStateService(tenantId, deviceId,
+ deviceStateService -> {
+ log.debug("[{}][{}] Forwarding device inactivity timeout update to local service. Updated inactivity timeout: [{}].", tenantId.getId(), deviceId.getId(), inactivityTimeout);
+ deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, inactivityTimeout);
+ },
+ () -> {
+ log.debug("[{}][{}] Sending device inactivity timeout update message to core. Updated inactivity timeout: [{}].", tenantId.getId(), deviceId.getId(), inactivityTimeout);
+ var deviceInactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
+ .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
+ .setInactivityTimeout(inactivityTimeout)
+ .build();
+ return TransportProtos.ToCoreMsg.newBuilder()
+ .setDeviceInactivityTimeoutUpdateMsg(deviceInactivityTimeoutUpdateMsg)
+ .build();
+ }, callback);
+ }
+
+ private void forwardToDeviceStateService(
+ TenantId tenantId, DeviceId deviceId,
+ Consumer toDeviceStateService,
+ Supplier toCore,
+ TbCallback callback
+ ) {
+ TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
+ if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) {
+ try {
+ toDeviceStateService.accept(deviceStateService.get());
+ } catch (Exception e) {
+ log.error("[{}][{}] Failed to process device connectivity event.", tenantId.getId(), deviceId.getId(), e);
+ callback.onFailure(e);
+ return;
+ }
+ callback.onSuccess();
+ } else {
+ TransportProtos.ToCoreMsg toCoreMsg = toCore.get();
+ clusterService.pushMsgToCore(tpi, deviceId.getId(), toCoreMsg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure));
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
index 11819ce5d8..a57debf012 100644
--- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -96,6 +96,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
@@ -129,11 +130,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService PERSISTENT_TELEMETRY_KEYS = Arrays.asList(
new EntityKey(EntityKeyType.TIME_SERIES, LAST_ACTIVITY_TIME),
new EntityKey(EntityKeyType.TIME_SERIES, INACTIVITY_ALARM_TIME),
- new EntityKey(EntityKeyType.TIME_SERIES, INACTIVITY_TIMEOUT),
new EntityKey(EntityKeyType.TIME_SERIES, ACTIVITY_STATE),
new EntityKey(EntityKeyType.TIME_SERIES, LAST_CONNECT_TIME),
new EntityKey(EntityKeyType.TIME_SERIES, LAST_DISCONNECT_TIME),
- new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT));
+ new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT)); // inactivity timeout is always a server attribute, even when activity data is stored as time series
private static final List PERSISTENT_ATTRIBUTE_KEYS = Arrays.asList(
new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME),
@@ -143,8 +143,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME,
- LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
+ public static final Set ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT = Set.of(
+ ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME
+ );
+
+ public static final Set ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT = Set.of(
+ ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT
+ );
+
private static final List PERSISTENT_ENTITY_FIELDS = Arrays.asList(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name"),
new EntityKey(EntityKeyType.ENTITY_FIELD, "type"),
@@ -251,7 +257,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService= deviceState.getLastActivityTime()) {
deviceState.setLastInactivityAlarmTime(0L);
- save(deviceId, INACTIVITY_ALARM_TIME, 0L);
+ save(state.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, 0L);
}
}
}
@@ -583,7 +589,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService fetchDeviceState(Device device) {
ListenableFuture future;
if (persistToTelemetry) {
- ListenableFuture> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES);
- future = Futures.transform(tsData, extractDeviceStateData(device), MoreExecutors.directExecutor());
+ ListenableFuture> timeseriesActivityDataFuture = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT);
+ ListenableFuture> inactivityTimeoutAttributeFuture = attributesService.find(
+ TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, INACTIVITY_TIMEOUT
+ );
+
+ ListenableFuture> fullActivityDataFuture = Futures.whenAllSucceed(timeseriesActivityDataFuture, inactivityTimeoutAttributeFuture).call(() -> {
+ List activityTimeseries = Futures.getDone(timeseriesActivityDataFuture);
+ Optional inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture);
+
+ List result;
+ if (inactivityTimeoutAttribute.isPresent()) {
+ result = new ArrayList<>(activityTimeseries.size() + 1);
+ result.addAll(activityTimeseries);
+ inactivityTimeoutAttribute.ifPresent(result::add);
+ } else {
+ return activityTimeseries;
+ }
+
+ return result;
+ }, deviceStateCallbackExecutor);
+
+ future = Futures.transform(fullActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor());
} else {
- ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
- future = Futures.transform(attrData, extractDeviceStateData(device), MoreExecutors.directExecutor());
+ ListenableFuture> attributesActivityDataFuture = attributesService.find(
+ TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT
+ );
+ future = Futures.transform(attributesActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor());
}
- return transformInactivityTimeout(future);
+ return future;
}
- private ListenableFuture transformInactivityTimeout(ListenableFuture future) {
- return Futures.transformAsync(future, deviceStateData -> {
- if (!persistToTelemetry || deviceStateData.getState().getInactivityTimeout() != defaultInactivityTimeoutMs) {
- return future; //fail fast
- }
- var attributesFuture = attributesService.find(TenantId.SYS_TENANT_ID, deviceStateData.getDeviceId(), AttributeScope.SERVER_SCOPE, INACTIVITY_TIMEOUT);
- return Futures.transform(attributesFuture, attributes -> {
- attributes.flatMap(KvEntry::getLongValue).ifPresent((inactivityTimeout) -> {
- if (inactivityTimeout > 0) {
- deviceStateData.getState().setInactivityTimeout(inactivityTimeout);
- }
- });
- return deviceStateData;
- }, MoreExecutors.directExecutor());
- }, deviceStateCallbackExecutor);
- }
-
- private Function, DeviceStateData> extractDeviceStateData(Device device) {
+ private Function, DeviceStateData> extractDeviceStateData(Device device) {
return new Function<>() {
@Nonnull
@Override
- public DeviceStateData apply(@Nullable List data) {
+ public DeviceStateData apply(@Nullable List extends KvEntry> data) {
try {
long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L);
@@ -690,7 +700,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService 0 ? inactivityTimeout : defaultInactivityTimeoutMs)
.build();
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName());
@@ -761,12 +771,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceStateService;
- private final TbClusterService clusterService;
-
- public DefaultRuleEngineDeviceStateManager(
- TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService,
- Optional deviceStateServiceOptional, TbClusterService clusterService
- ) {
- this.serviceInfoProvider = serviceInfoProvider;
- this.partitionService = partitionService;
- this.deviceStateService = deviceStateServiceOptional;
- this.clusterService = clusterService;
- }
-
- @Getter
- private abstract static class ConnectivityEventInfo {
-
- private final TenantId tenantId;
- private final DeviceId deviceId;
- private final long eventTime;
-
- private ConnectivityEventInfo(TenantId tenantId, DeviceId deviceId, long eventTime) {
- this.tenantId = tenantId;
- this.deviceId = deviceId;
- this.eventTime = eventTime;
- }
-
- abstract void forwardToLocalService();
-
- abstract TransportProtos.ToCoreMsg toQueueMsg();
-
- }
-
- @Override
- public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) {
- routeEvent(new ConnectivityEventInfo(tenantId, deviceId, connectTime) {
- @Override
- void forwardToLocalService() {
- deviceStateService.ifPresent(service -> service.onDeviceConnect(tenantId, deviceId, connectTime));
- }
-
- @Override
- TransportProtos.ToCoreMsg toQueueMsg() {
- var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder()
- .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
- .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
- .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
- .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
- .setLastConnectTime(connectTime)
- .build();
- return TransportProtos.ToCoreMsg.newBuilder()
- .setDeviceConnectMsg(deviceConnectMsg)
- .build();
- }
- }, callback);
- }
-
- @Override
- public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) {
- routeEvent(new ConnectivityEventInfo(tenantId, deviceId, activityTime) {
- @Override
- void forwardToLocalService() {
- deviceStateService.ifPresent(service -> service.onDeviceActivity(tenantId, deviceId, activityTime));
- }
-
- @Override
- TransportProtos.ToCoreMsg toQueueMsg() {
- var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder()
- .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
- .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
- .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
- .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
- .setLastActivityTime(activityTime)
- .build();
- return TransportProtos.ToCoreMsg.newBuilder()
- .setDeviceActivityMsg(deviceActivityMsg)
- .build();
- }
- }, callback);
- }
-
- @Override
- public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) {
- routeEvent(new ConnectivityEventInfo(tenantId, deviceId, disconnectTime) {
- @Override
- void forwardToLocalService() {
- deviceStateService.ifPresent(service -> service.onDeviceDisconnect(tenantId, deviceId, disconnectTime));
- }
-
- @Override
- TransportProtos.ToCoreMsg toQueueMsg() {
- var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
- .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
- .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
- .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
- .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
- .setLastDisconnectTime(disconnectTime)
- .build();
- return TransportProtos.ToCoreMsg.newBuilder()
- .setDeviceDisconnectMsg(deviceDisconnectMsg)
- .build();
- }
- }, callback);
- }
-
- @Override
- public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) {
- routeEvent(new ConnectivityEventInfo(tenantId, deviceId, inactivityTime) {
- @Override
- void forwardToLocalService() {
- deviceStateService.ifPresent(service -> service.onDeviceInactivity(tenantId, deviceId, inactivityTime));
- }
-
- @Override
- TransportProtos.ToCoreMsg toQueueMsg() {
- var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
- .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
- .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
- .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
- .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
- .setLastInactivityTime(inactivityTime)
- .build();
- return TransportProtos.ToCoreMsg.newBuilder()
- .setDeviceInactivityMsg(deviceInactivityMsg)
- .build();
- }
- }, callback);
- }
-
- private void routeEvent(ConnectivityEventInfo eventInfo, TbCallback callback) {
- var tenantId = eventInfo.getTenantId();
- var deviceId = eventInfo.getDeviceId();
- long eventTime = eventInfo.getEventTime();
-
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
- if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) {
- log.debug("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime);
- try {
- eventInfo.forwardToLocalService();
- } catch (Exception e) {
- log.error("[{}][{}] Failed to process device connectivity event. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime, e);
- callback.onFailure(e);
- return;
- }
- callback.onSuccess();
- } else {
- TransportProtos.ToCoreMsg msg = eventInfo.toQueueMsg();
- log.debug("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime);
- clusterService.pushMsgToCore(tpi, UUID.randomUUID(), msg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure));
- }
- }
-
-}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
index 0eb04926f6..3e2c873030 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
@@ -160,9 +160,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
@Override
public void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts, TbCallback callback) {
onTimeSeriesUpdate(entityId, ts);
- if (entityId.getEntityType() == EntityType.DEVICE) {
- updateDeviceInactivityTimeout(tenantId, entityId, ts);
- }
callback.onSuccess();
}
@@ -170,9 +167,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
public void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, TbCallback callback) {
onTimeSeriesUpdate(entityId,
keys.stream().map(key -> new BasicTsKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList()));
- if (entityId.getEntityType() == EntityType.DEVICE) {
- deleteDeviceInactivityTimeout(tenantId, entityId, keys);
- }
callback.onSuccess();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
index 3fc314b632..9e9ca42e83 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
@@ -33,7 +33,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
-import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
@@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.ComparisonTsValue;
-import org.thingsboard.server.common.data.query.OriginatorAlarmFilter;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
@@ -55,17 +54,16 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
-import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd;
-import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
@@ -74,7 +72,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.GetTsCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
-import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import java.util.ArrayList;
import java.util.Arrays;
@@ -83,7 +80,6 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
@@ -430,13 +426,25 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
long start = System.currentTimeMillis();
ctx.fetchData();
long end = System.currentTimeMillis();
- stats.getAlarmQueryInvocationCnt().incrementAndGet();
- stats.getAlarmQueryTimeSpent().addAndGet(end - start);
- TbAlarmCountSubCtx finalCtx = ctx;
- ScheduledFuture> task = scheduler.scheduleWithFixedDelay(
- () -> refreshDynamicQuery(finalCtx),
- dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
- finalCtx.setRefreshTask(task);
+ stats.getRegularQueryInvocationCnt().incrementAndGet();
+ stats.getRegularQueryTimeSpent().addAndGet(end - start);
+ Set entitiesIds = ctx.getEntitiesIds();
+ ctx.cancelTasks();
+ ctx.clearAlarmSubscriptions();
+ if (entitiesIds != null && entitiesIds.isEmpty()) {
+ AlarmCountUpdate update = new AlarmCountUpdate(cmd.getCmdId(), 0);
+ ctx.sendWsMsg(update);
+ } else {
+ ctx.doFetchAlarmCount();
+ if (entitiesIds != null) {
+ ctx.createAlarmSubscriptions();
+ }
+ TbAlarmCountSubCtx finalCtx = ctx;
+ ScheduledFuture> task = scheduler.scheduleWithFixedDelay(
+ () -> refreshDynamicQuery(finalCtx),
+ dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
+ finalCtx.setRefreshTask(task);
+ }
} else {
log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd);
}
@@ -555,7 +563,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) {
Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
- attributesService, stats, alarmService, sessionRef, cmd.getCmdId());
+ attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription, maxAlarmQueriesPerRefreshInterval);
if (cmd.getQuery() != null) {
ctx.setAndResolveQuery(cmd.getQuery());
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java
index ae3c2e0ccd..6689c11c42 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java
@@ -19,49 +19,152 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
+import org.thingsboard.server.common.data.query.EntityData;
+import org.thingsboard.server.common.data.query.EntityDataPageLink;
+import org.thingsboard.server.common.data.query.EntityDataQuery;
+import org.thingsboard.server.common.data.query.EntityDataSortOrder;
+import org.thingsboard.server.common.data.query.EntityKey;
+import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
+import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
@Slf4j
@ToString(callSuper = true)
public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx {
private final AlarmService alarmService;
+ protected final Map subToEntityIdMap;
+
+ @Getter
+ private LinkedHashSet entitiesIds;
+
+ private final int maxEntitiesPerAlarmSubscription;
+
+ private final int maxAlarmQueriesPerRefreshInterval;
+
@Getter
@Setter
private volatile int result;
+ @Getter
+ @Setter
+ private boolean tooManyEntities;
+
+ private int alarmCountInvocationAttempts;
+
public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService,
- WebSocketSessionRef sessionRef, int cmdId) {
+ WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription, int maxAlarmQueriesPerRefreshInterval) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.alarmService = alarmService;
+ this.subToEntityIdMap = new ConcurrentHashMap<>();
+ this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription;
+ this.maxAlarmQueriesPerRefreshInterval = maxAlarmQueriesPerRefreshInterval;
+ this.entitiesIds = null;
+ }
+
+ @Override
+ public void clearSubscriptions() {
+ clearAlarmSubscriptions();
}
@Override
public void fetchData() {
- result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query);
- sendWsMsg(new AlarmCountUpdate(cmdId, result));
+ resetInvocationCounter();
+ if (query.getEntityFilter() != null) {
+ entitiesIds = new LinkedHashSet<>();
+ log.trace("[{}] Fetching data: {}", cmdId, alarmCountInvocationAttempts);
+ PageData data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery());
+ entitiesIds.clear();
+ tooManyEntities = data.hasNext();
+ for (EntityData entityData : data.getData()) {
+ entitiesIds.add(entityData.getEntityId());
+ }
+ }
}
@Override
protected void update() {
- int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query);
- if (newCount != result) {
- result = newCount;
- sendWsMsg(new AlarmCountUpdate(cmdId, result));
- }
+ resetInvocationCounter();
+ fetchAlarmCount();
}
@Override
public boolean isDynamic() {
return true;
}
+
+ public void fetchAlarmCount() {
+ alarmCountInvocationAttempts++;
+ log.trace("[{}] Fetching alarms: {}", cmdId, alarmCountInvocationAttempts);
+ if (alarmCountInvocationAttempts <= maxAlarmQueriesPerRefreshInterval) {
+ int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds);
+ if (newCount != result) {
+ result = newCount;
+ sendWsMsg(new AlarmCountUpdate(cmdId, result));
+ }
+ } else {
+ log.trace("[{}] Ignore alarm count fetch due to rate limit: [{}] of maximum [{}]", cmdId, alarmCountInvocationAttempts, maxAlarmQueriesPerRefreshInterval);
+ }
+ }
+
+ public void doFetchAlarmCount() {
+ result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds);
+ sendWsMsg(new AlarmCountUpdate(cmdId, result));
+ }
+
+ private EntityDataQuery buildEntityDataQuery() {
+ EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null,
+ new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)));
+ return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
+ }
+
+ private void resetInvocationCounter() {
+ alarmCountInvocationAttempts = 0;
+ }
+
+ public void createAlarmSubscriptions() {
+ for (EntityId entityId : entitiesIds) {
+ createAlarmSubscriptionForEntity(entityId);
+ }
+ }
+
+ private void createAlarmSubscriptionForEntity(EntityId entityId) {
+ int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
+ subToEntityIdMap.put(subIdx, entityId);
+ log.trace("[{}][{}][{}] Creating alarms subscription for [{}] ", serviceId, cmdId, subIdx, entityId);
+ TbAlarmsSubscription subscription = TbAlarmsSubscription.builder()
+ .serviceId(serviceId)
+ .sessionId(sessionRef.getSessionId())
+ .subscriptionId(subIdx)
+ .tenantId(sessionRef.getSecurityCtx().getTenantId())
+ .entityId(entityId)
+ .updateProcessor((sub, update) -> fetchAlarmCount())
+ .build();
+ localSubscriptionService.addSubscription(subscription, sessionRef);
+ }
+
+ public void clearAlarmSubscriptions() {
+ if (subToEntityIdMap != null) {
+ for (Integer subId : subToEntityIdMap.keySet()) {
+ localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId);
+ }
+ subToEntityIdMap.clear();
+ }
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
index f6ce2a6a6b..f6b4067543 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
@@ -39,6 +39,7 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
@@ -359,7 +360,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx {
EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder();
EntityDataSortOrder entitiesSortOrder;
if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) {
- entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY));
+ entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME));
} else {
entitiesSortOrder = sortOrder;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
index 178dde33e2..06fe7f4036 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
@@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
+import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@@ -61,6 +62,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final Map> importServices = new HashMap<>();
private final RelationService relationService;
+ private final CalculatedFieldService calculatedFieldService;
private final RateLimitService rateLimitService;
private final TbLogEntityActionService logEntityActionService;
@@ -72,7 +74,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE
);
-
@Override
public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException {
if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) {
@@ -129,13 +130,11 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
}
}
-
@Override
public Comparator getEntityTypeComparatorForImport() {
return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf);
}
-
@SuppressWarnings("unchecked")
private , D extends EntityExportData> EntityExportService getExportService(EntityType entityType) {
EntityExportService, ?, ?> exportService = exportServices.get(entityType);
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
index 2b7e7d3593..b77447df8d 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
@@ -32,7 +32,6 @@ public interface EntitiesExportImportService {
, I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException;
-
void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException;
Comparator getEntityTypeComparatorForImport();
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
index b0bea68290..5c2fcc7dc6 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasVersion;
+import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
@@ -59,6 +61,8 @@ public class DefaultEntityExportService ctx, I entityId) throws ThingsboardException {
@@ -98,6 +102,10 @@ public class DefaultEntityExportService> attributes = exportAttributes(ctx, entity);
exportData.setAttributes(attributes);
}
+ if (ctx.getSettings().isExportCalculatedFields()) {
+ List calculatedFields = exportCalculatedFields(ctx, entity.getId());
+ exportData.setCalculatedFields(calculatedFields);
+ }
}
private List exportRelations(EntitiesExportCtx> ctx, E entity) throws ThingsboardException {
@@ -141,6 +149,19 @@ public class DefaultEntityExportService exportCalculatedFields(EntitiesExportCtx> ctx, EntityId entityId) {
+ List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId);
+ calculatedFields.forEach(calculatedField -> {
+ calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId));
+ calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
+ if (argument.getRefEntityId() != null) {
+ argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId()));
+ }
+ });
+ });
+ return calculatedFields;
+ }
+
protected ID getExternalIdOrElseInternal(EntitiesExportCtx> ctx, ID internalId) {
if (internalId == null || internalId.isNullUid()) return internalId;
var result = ctx.getExternalId(internalId);
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
index 00ce23b2e8..7cd4c3aca1 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
@@ -47,7 +47,11 @@ public class AssetImportService extends BaseEntityImportService exportData, IdProvider idProvider) {
- return assetService.saveAsset(asset);
+ Asset savedAsset = assetService.saveAsset(asset);
+ if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
+ importCalculatedFields(ctx, savedAsset, exportData, idProvider);
+ }
+ return savedAsset;
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java
index 97c54957bf..32a0090a4a 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java
@@ -50,7 +50,11 @@ public class AssetProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider) {
- return assetProfileService.saveAssetProfile(assetProfile);
+ AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile);
+ if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
+ importCalculatedFields(ctx, saved, exportData, idProvider);
+ }
+ return saved;
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
index 7044b235a0..bfa95af83c 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
@@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.HasDefaultOption;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.service.action.EntityActionService;
@@ -78,6 +80,8 @@ public abstract class BaseEntityImportService existing = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), savedEntity.getId());
+ List fieldsToSave = exportData.getCalculatedFields().stream()
+ .peek(calculatedField -> {
+ calculatedField.setTenantId(ctx.getTenantId());
+ calculatedField.setEntityId(savedEntity.getId());
+ calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
+ if (argument.getRefEntityId() != null) {
+ argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt()));
+ }
+ });
+ }).toList();
+
+ for (CalculatedField existingField : existing) {
+ boolean found = fieldsToSave.stream().anyMatch(importedField -> compareCalculatedFields(existingField, importedField));
+ if (!found) {
+ calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId());
+ updated = true;
+ }
+ }
+
+ for (CalculatedField calculatedField : fieldsToSave) {
+ boolean found = existing.stream().anyMatch(existingField -> compareCalculatedFields(existingField, calculatedField));
+ if (!found) {
+ calculatedFieldService.save(calculatedField);
+ updated = true;
+ }
+ }
+ return updated;
+ }
+
+ private boolean compareCalculatedFields(CalculatedField existingField, CalculatedField newField) {
+ CalculatedField oldCopy = new CalculatedField(existingField);
+ CalculatedField newCopy = new CalculatedField(newField);
+ oldCopy.setId(null);
+ newCopy.setId(null);
+ oldCopy.setVersion(null);
+ newCopy.setVersion(null);
+ oldCopy.setCreatedTime(0);
+ newCopy.setCreatedTime(0);
+ return oldCopy.equals(newCopy);
+ }
+
protected void onEntitySaved(User user, E savedEntity, E oldEntity) throws ThingsboardException {
logEntityActionService.logEntityAction(user.getTenantId(), savedEntity.getId(), savedEntity, null,
oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, user);
}
-
@SuppressWarnings("unchecked")
protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) {
return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
@@ -313,10 +364,10 @@ public abstract class BaseEntityImportService new MissingEntityException(externalId));
}
-
@SuppressWarnings("unchecked")
@RequiredArgsConstructor
protected class IdProvider {
+
private final EntitiesImportCtx ctx;
private final EntityImportResult importResult;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
index 61b9839cbd..84e264efdd 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
@@ -64,13 +64,18 @@ public class DeviceImportService extends BaseEntityImportService exportData, IdProvider idProvider) {
- return deviceProfileService.saveDeviceProfile(deviceProfile);
+ DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
+ if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
+ importCalculatedFields(ctx, saved, exportData, idProvider);
+ }
+ return saved;
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
index eed18abe52..89b51ce84a 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
@@ -94,7 +94,6 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
-import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.transform;
import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName;
@@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials())
+ .saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(false)
.build());
ctx.setFinalImportAttempt(true);
@@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
var sw = TbStopWatch.create("before");
List entityTypes = request.getEntityTypes().keySet().stream()
- .sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList());
+ .sorted(exportImportService.getEntityTypeComparatorForImport()).toList();
for (EntityType entityType : entityTypes) {
log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
sw.startNew("Entities " + entityType.name());
@@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials())
+ .saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(config.isFindExistingEntityByName())
.build();
}
@@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
@Override
- public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception {
+ public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) {
HasId entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId);
if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type");
@@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.exportRelations(otherVersion.hasRelations())
.exportAttributes(otherVersion.hasAttributes())
.exportCredentials(otherVersion.hasCredentials())
+ .exportCalculatedFields(otherVersion.hasCalculatedFields())
.build());
EntityExportData> currentVersion;
try {
@@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override
- public ListenableFuture> listBranches(TenantId tenantId) throws Exception {
+ public ListenableFuture> listBranches(TenantId tenantId) {
return gitServiceQueue.listBranches(tenantId);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java
index 669dad347a..b3007c6738 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java
@@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx {
.exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials())
+ .exportCalculatedFields(config.isSaveCalculatedFields())
.build();
}
@@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx {
log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId);
externalIdMap.put(internalId, externalId != null ? externalId : internalId);
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
index ab836fa13d..e24683cd5a 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
@@ -91,6 +91,10 @@ public class EntitiesImportCtx {
return getSettings().isSaveCredentials();
}
+ public boolean isSaveCalculatedFields() {
+ return getSettings().isSaveCalculatedFields();
+ }
+
public EntityId getInternalId(EntityId externalId) {
var result = externalToInternalIdMap.get(externalId);
log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss");
@@ -140,5 +144,4 @@ public class EntitiesImportCtx {
return notFoundIds.contains(externalId);
}
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java
index e0a5b40281..55c8a763b0 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java
@@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx
.exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials())
+ .exportCalculatedFields(config.isSaveCalculatedFields())
.build();
this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index 252f696c72..0201bb4942 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -370,7 +370,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess) {
- DonAsynchron.withCallback(saveFuture, onSuccess, null, tsCallBackExecutor);
+ addMainCallback(saveFuture, onSuccess, null);
}
private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess, Consumer onFailure) {
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java
index 62913a187b..4b60ec923a 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java
@@ -93,7 +93,8 @@ public class TbCoreTransportApiService {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
log.info("Received application ready event. Starting polling for events.");
- transportApiTemplate.init(transportApiService);
+ transportApiTemplate.subscribe();
+ transportApiTemplate.launch(transportApiService);
}
@PreDestroy
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
index 90f6b36e0f..7288a8bac9 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
@@ -229,6 +229,7 @@ public class DefaultWebSocketService implements WebSocketService {
} catch (TbRateLimitsException e) {
log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e);
} catch (Exception e) {
+ sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, e.getMessage());
log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e);
}
}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 4dfc51807a..b01a0cf55e 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -512,6 +512,8 @@ actors:
enabled: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
# The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour
configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}"
+ # Time in seconds to receive calculation result.
+ calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}"
debug:
settings:
@@ -1597,6 +1599,16 @@ queue:
- key: max.poll.records
# Amount of records to be returned in a single poll. For Housekeeper reprocessing topic, we should consume messages (tasks) one by one
value: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_MAX_POLL_RECORDS:1}"
+ edqs.events:
+ # Key-value properties for Kafka consumer for edqs.events topic
+ - key: max.poll.records
+ # Max poll records for edqs.events topic
+ value: "${TB_QUEUE_KAFKA_EDQS_EVENTS_MAX_POLL_RECORDS:512}"
+ edqs.state:
+ # Key-value properties for Kafka consumer for edqs.state topic
+ - key: max.poll.records
+ # Max poll records for edqs.state topic
+ value: "${TB_QUEUE_KAFKA_EDQS_STATE_MAX_POLL_RECORDS:512}"
other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000"
other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside
# - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
@@ -1630,6 +1642,12 @@ queue:
calculated-field: "${TB_QUEUE_KAFKA_CF_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for Calculated Field State topics
calculated-field-state: "${TB_QUEUE_KAFKA_CF_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:104857600000;partitions:1;min.insync.replicas:1;cleanup.policy:compact}"
+ # Kafka properties for EDQS events topics. Partitions number must be the same as queue.edqs.partitions
+ edqs-events: "${TB_QUEUE_KAFKA_EDQS_EVENTS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1}"
+ # Kafka properties for EDQS requests topic (default: 3 minutes retention). Partitions number must be the same as queue.edqs.partitions
+ edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:12;min.insync.replicas:1}"
+ # Kafka properties for EDQS state topic (infinite retention, compaction). Partitions number must be the same as queue.edqs.partitions
+ edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:12;min.insync.replicas:1;cleanup.policy:compact}"
consumer-stats:
# Prints lag between consumer group offset and last messages offset in Kafka topics
enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}"
@@ -1659,6 +1677,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
+ # For high-priority notifications that require minimum latency and processing time
+ notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@@ -1703,6 +1723,44 @@ queue:
enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}"
# Statistics printing interval for Housekeeper
print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}"
+ edqs:
+ sync:
+ # Enable/disable EDQS synchronization
+ enabled: "${TB_EDQS_SYNC_ENABLED:false}"
+ # Batch size of entities being synced with EDQS
+ entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}"
+ # Batch size of timeseries data being synced with EDQS
+ ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}"
+ api:
+ # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation)
+ supported: "${TB_EDQS_API_SUPPORTED:false}"
+ # Whether to auto-enable EDQS API (if queue.edqs.api.supported is true) when sync of data to Kafka is finished
+ auto_enable: "${TB_EDQS_API_AUTO_ENABLE:true}"
+ # Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices)
+ mode: "${TB_EDQS_MODE:local}"
+ local:
+ # Path to RocksDB for EDQS backup when running in local mode
+ rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:${user.home}/.rocksdb/edqs}"
+ # Number of partitions for EDQS topics
+ partitions: "${TB_EDQS_PARTITIONS:12}"
+ # EDQS partitioning strategy: tenant (partition is resolved by tenant id) or none (no specific strategy, resolving by message key)
+ partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}"
+ # EDQS requests topic
+ requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}"
+ # EDQS responses topic
+ responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}"
+ # Poll interval for EDQS topics
+ poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}"
+ # Maximum amount of pending requests to EDQS
+ max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
+ # Maximum timeout for requests to EDQS
+ max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}"
+ stats:
+ # Enable/disable statistics for EDQS
+ enabled: "${TB_EDQS_STATS_ENABLED:true}"
+ # Statistics printing interval for EDQS
+ print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}"
+
vc:
# Default topic name
topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}"
@@ -1734,6 +1792,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
+ # For high-priority notifications that require minimum latency and processing time
+ notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine
@@ -1754,6 +1814,8 @@ queue:
event_topic: "${TB_QUEUE_CF_EVENT_TOPIC:tb_cf_event}"
# Topic name for Calculated Field (CF) compacted states
state_topic: "${TB_QUEUE_CF_STATE_TOPIC:tb_cf_state}"
+ # For high-priority notifications that require minimum latency and processing time
+ notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}"
# Interval in milliseconds to poll messages by CF (Rule Engine) microservices
poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}"
# Amount of partitions used by CF microservices
@@ -1772,6 +1834,10 @@ queue:
edge:
# Default topic name
topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}"
+ # For high-priority notifications that require minimum latency and processing time
+ notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}"
+ # For edge events messages
+ event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}"
# Amount of partitions used by Edge services
partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}"
# Poll interval for topics related to Edge services
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 975c3b426b..e90aecb41e 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -402,7 +402,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
} catch (Exception e) {
throw new RuntimeException(e);
}
- Awaitility.await("all tasks processed").atMost(60, TimeUnit.SECONDS).during(300, TimeUnit.MILLISECONDS)
+ Awaitility.await("all tasks processed").atMost(90, TimeUnit.SECONDS).during(300, TimeUnit.MILLISECONDS)
.until(() -> storage.getLag("tb_housekeeper") == 0);
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
new file mode 100644
index 0000000000..ce5221ab89
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
@@ -0,0 +1,72 @@
+/**
+ * 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.controller;
+
+import org.junit.Before;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.query.EntityCountQuery;
+import org.thingsboard.server.common.data.query.EntityData;
+import org.thingsboard.server.common.data.query.EntityDataQuery;
+import org.thingsboard.server.common.msg.edqs.EdqsApiService;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.edqs.state.EdqsStateService;
+import org.thingsboard.server.edqs.util.EdqsRocksDb;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.awaitility.Awaitility.await;
+
+@DaoSqlTest
+@TestPropertySource(properties = {
+// "queue.type=kafka", // uncomment to use Kafka
+// "queue.kafka.bootstrap.servers=10.7.1.254:9092",
+ "queue.edqs.sync.enabled=true",
+ "queue.edqs.api.supported=true",
+ "queue.edqs.api.auto_enable=true",
+ "queue.edqs.mode=local"
+})
+public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
+
+ @Autowired
+ private EdqsApiService edqsApiService;
+
+ @Autowired
+ private EdqsStateService edqsStateService;
+
+ @MockBean // so that we don't do backup for tests
+ private EdqsRocksDb edqsRocksDb;
+
+ @Before
+ public void before() {
+ await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsApiService.isEnabled() && edqsStateService.isReady());
+ }
+
+ @Override
+ protected PageData findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) {
+ return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findByQuery(query),
+ result -> result.getTotalElements() == expectedResultSize);
+ }
+
+ @Override
+ protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult) {
+ return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query),
+ result -> result == expectedResult);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
index ac618d7f96..011399e883 100644
--- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
@@ -18,12 +18,14 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.awaitility.Awaitility;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions;
+import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@@ -49,6 +51,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
+import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
@@ -70,9 +73,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@@ -130,36 +135,25 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
filter.setDeviceNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter);
-
- Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(97, count.longValue());
+ countByQueryAndCheck(countQuery, 97);
filter.setDeviceTypes(List.of("unknown"));
- count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(0, count.longValue());
+ countByQueryAndCheck(countQuery, 0);
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("Device1");
-
- count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(11, count.longValue());
+ countByQueryAndCheck(countQuery, 11);
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.DEVICE);
entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList()));
-
countQuery = new EntityCountQuery(entityListFilter);
-
- count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(97, count.longValue());
+ countByQueryAndCheck(countQuery, 97);
EntityTypeFilter filter2 = new EntityTypeFilter();
filter2.setEntityType(EntityType.DEVICE);
-
- EntityCountQuery countQuery2 = new EntityCountQuery(filter2);
-
- Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class);
- Assert.assertEquals(97, count2.longValue());
+ countQuery = new EntityCountQuery(filter2);
+ countByQueryAndCheck(countQuery, 97);
}
@Test
@@ -169,51 +163,44 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter allDeviceFilter = new EntityTypeFilter();
allDeviceFilter.setEntityType(EntityType.DEVICE);
EntityCountQuery query = new EntityCountQuery(allDeviceFilter);
- Long initialCount = doPostWithResponse("/api/entitiesQuery/count", query, Long.class);
+ countByQueryAndCheck(query, 0);
loginTenantAdmin();
List devices = new ArrayList<>();
+ String devicePrefix = "Device" + RandomStringUtils.randomAlphabetic(5);
for (int i = 0; i < 97; i++) {
Device device = new Device();
- device.setName("Device" + i);
+ device.setName(devicePrefix + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
DeviceTypeFilter filter = new DeviceTypeFilter();
- filter.setDeviceType("default");
+ filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("");
loginSysAdmin();
EntityCountQuery countQuery = new EntityCountQuery(filter);
+ countByQueryAndCheck(countQuery, 97);
- Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(97, count.longValue());
-
- filter.setDeviceType("unknown");
- count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(0, count.longValue());
-
- filter.setDeviceType("default");
- filter.setDeviceNameFilter("Device1");
+ filter.setDeviceTypes(List.of("unknown"));
+ countByQueryAndCheck(countQuery, 0);
- count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(11, count.longValue());
+ filter.setDeviceTypes(List.of("default"));
+ filter.setDeviceNameFilter(devicePrefix + "1");
+ countByQueryAndCheck(countQuery, 11);
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.DEVICE);
entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList()));
countQuery = new EntityCountQuery(entityListFilter);
+ countByQueryAndCheck(countQuery, 97);
- count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(97, count.longValue());
-
- Long count2 = doPostWithResponse("/api/entitiesQuery/count", query, Long.class);
- Assert.assertEquals(initialCount + 97, count2.longValue());
+ countByQueryAndCheck(countQuery, 97);
}
@Test
@@ -371,11 +358,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- PageData data =
- doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
-
- Assert.assertEquals(97, data.getTotalElements());
+ PageData data = findByQueryAndCheck(query, 97);
Assert.assertEquals(10, data.getTotalPages());
Assert.assertTrue(data.hasNext());
Assert.assertEquals(10, data.getData().size());
@@ -383,8 +366,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) {
query = query.next();
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
+ data = findByQuery(query);
loadedEntities.addAll(data.getData());
}
Assert.assertEquals(97, loadedEntities.size());
@@ -406,8 +388,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
+ data = findByQuery(query);
Assert.assertEquals(11, data.getTotalElements());
Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue());
@@ -423,9 +404,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null);
- PageData data2 =
- doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference>() {
- });
+ PageData data2 = findByQuery(query2);
Assert.assertEquals(97, data2.getTotalElements());
Assert.assertEquals(10, data2.getTotalPages());
@@ -473,20 +452,15 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
-
- PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
-
- Assert.assertEquals(87, data.getTotalElements());
+ findByQueryAndCheck(query, 87);
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
- Assert.assertEquals(10, data.getTotalElements());
+ findByQueryAndCheck(query, 10);
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
- Assert.assertEquals(87, data.getTotalElements());
+ findByQueryAndCheck(query, 87);
}
private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) {
@@ -531,14 +505,12 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
- PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
+ PageData data = findByQueryAndCheck(query, 67);
List loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) {
query = query.next();
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
+ data = findByQuery(query);
loadedEntities.addAll(data.getData());
}
Assert.assertEquals(67, loadedEntities.size());
@@ -551,6 +523,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
+ highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate();
predicate.setValue(FilterPredicateValue.fromDouble(45));
predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
@@ -559,13 +532,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
+ data = findByQuery(query);
loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) {
query = query.next();
- data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
+ data = findByQuery(query);
loadedEntities.addAll(data.getData());
}
Assert.assertEquals(highTemperatures.size(), loadedEntities.size());
@@ -604,6 +575,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "alarmActiveTime"));
+ highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate();
DynamicValue dynamicValue =
@@ -627,16 +599,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
- Awaitility.await()
+ await()
.alias("data by query")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
- var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {});
+ var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == numOfDevices;
});
- var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {});
+ var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(numOfDevices, loadedEntities.size());
@@ -694,11 +666,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null);
- PageData data =
- doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
-
- Assert.assertEquals(97, data.getTotalElements());
+ PageData data = findByQueryAndCheck(query, 97);
Assert.assertEquals(10, data.getTotalPages());
Assert.assertTrue(data.hasNext());
Assert.assertEquals(10, data.getData().size());
@@ -712,9 +680,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
});
EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter);
-
- Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
- Assert.assertEquals(97, count.longValue());
+ countByQueryAndCheck(countQuery, 97);
}
@Test
@@ -742,28 +708,29 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30);
KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME);
KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName");
- KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT");
+ KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT");
KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER");
// all devices with ownerName = TEST TENANT
- EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter));
- checkEntitiesCount(query, numOfDevices);
+ EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter));
+ await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query),
+ result -> result == numOfDevices);
// all devices with ownerName = TEST TENANT
- EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter));
- checkEntitiesCount(activeAlarmTimeToLongQuery, 0);
+ EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter));
+ countByQueryAndCheck(activeAlarmTimeToLongQuery, 0);
// all devices with wrong ownerName
EntityCountQuery wrongTenantNameQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter));
- checkEntitiesCount(wrongTenantNameQuery, 0);
+ countByQueryAndCheck(wrongTenantNameQuery, 0);
// all devices with owner type = TENANT
EntityCountQuery tenantEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter));
- checkEntitiesCount(tenantEntitiesQuery, numOfDevices);
+ countByQueryAndCheck(tenantEntitiesQuery, numOfDevices);
// all devices with owner type = CUSTOMER
EntityCountQuery customerEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter));
- checkEntitiesCount(customerEntitiesQuery, 0);
+ countByQueryAndCheck(customerEntitiesQuery, 0);
}
@Test
@@ -790,7 +757,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5);
KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME);
KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName");
- KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT");
+ KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT");
KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER");
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@@ -851,41 +818,29 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- PageData data =
- doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
-
- Assert.assertEquals(1, data.getTotalElements());
- Assert.assertEquals(1, data.getTotalPages());
- Assert.assertEquals(1, data.getData().size());
+ findByQueryAndCheck(query, 1);
// unnassign dashboard
login(TENANT_EMAIL, TENANT_PASSWORD);
doDelete("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
- PageData dataAfterUnassign =
- doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {
- });
-
- Assert.assertEquals(0, dataAfterUnassign.getTotalElements());
- Assert.assertEquals(0, dataAfterUnassign.getTotalPages());
- Assert.assertEquals(0, dataAfterUnassign.getData().size());
+ findByQueryAndCheck(query, 0);
}
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception {
- Awaitility.await()
+ await()
.alias("data by query")
.atMost(30, TimeUnit.SECONDS)
.until(() -> {
- var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {});
+ var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == expectedNumOfDevices;
});
- if (expectedNumOfDevices == 0) {
- return;
- }
- var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference>() {});
+ if (expectedNumOfDevices == 0) {
+ return;
+ }
+ var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(expectedNumOfDevices, loadedEntities.size());
@@ -898,25 +853,37 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue();
Assert.assertEquals("Device" + i, name);
- Assert.assertEquals( expectedOwnerName, ownerName);
- Assert.assertEquals( expectedOwnerType, ownerType);
+ Assert.assertEquals(expectedOwnerName, ownerName);
+ Assert.assertEquals(expectedOwnerType, ownerType);
Assert.assertEquals("1" + i, alarmActiveTime);
}
}
- private void checkEntitiesCount(EntityCountQuery query, int expectedNumOfDevices) {
- Awaitility.await()
- .alias("count by query")
- .atMost(30, TimeUnit.SECONDS)
- .until(() -> {
- var count = doPost("/api/entitiesQuery/count", query, Integer.class);
- return count == expectedNumOfDevices;
- });
- }
+ protected PageData findByQuery(EntityDataQuery query) throws Exception {
+ return doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {
+ });
+ }
+
+ protected PageData findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) throws Exception {
+ PageData result = findByQuery(query);
+ assertThat(result.getTotalElements()).isEqualTo(expectedResultSize);
+ return result;
+ }
+
+ protected Long countByQuery(EntityCountQuery countQuery) throws Exception {
+ return doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
+ }
+
+ protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult) throws Exception {
+ Long result = countByQuery(query);
+ assertThat(result).isEqualTo(expectedResult);
+ return result;
+ }
private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) {
KeyFilter tenantOwnerNameFilter = new KeyFilter();
tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName));
+ tenantOwnerNameFilter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate ownerNamePredicate = new StringFilterPredicate();
ownerNamePredicate.setValue(FilterPredicateValue.fromString(value));
ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL);
@@ -927,6 +894,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) {
KeyFilter numericFilter = new KeyFilter();
numericFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, attribute));
+ numericFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate();
predicate.setValue(FilterPredicateValue.fromDouble(value));
predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
index c33e9f7202..9801907d3b 100644
--- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
@@ -83,7 +83,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Slf4j
@DaoSqlTest
@TestPropertySource(properties = {
- "server.ws.alarms_per_alarm_status_subscription_cache_size=5"
+ "server.ws.alarms_per_alarm_status_subscription_cache_size=5",
+ "server.ws.dynamic_page_link.refresh_interval=15"
})
public class WebsocketApiTest extends AbstractControllerTest {
@Autowired
@@ -324,6 +325,83 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertEquals(1, update.getCount());
}
+ @Test
+ public void testAlarmCountWsCmdWithSingleEntityFilter() throws Exception {
+ loginTenantAdmin();
+
+ SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
+ singleEntityFilter.setSingleEntity(tenantId);
+ AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter);
+ AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
+
+ getWsClient().send(cmd1);
+
+ AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
+ Assert.assertEquals(1, update.getCmdId());
+ Assert.assertEquals(0, update.getCount());
+
+ //create alarm, check count = 1
+ getWsClient().registerWaitForUpdate();
+
+ Alarm alarm = new Alarm();
+ alarm.setOriginator(tenantId);
+ alarm.setType("TEST ALARM");
+ alarm.setSeverity(AlarmSeverity.WARNING);
+ alarm = doPost("/api/alarm", alarm, Alarm.class);
+
+ update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate());
+ Assert.assertEquals(1, update.getCmdId());
+ Assert.assertEquals(1, update.getCount());
+
+ // set wrong entity id in filter, check count = 0
+ singleEntityFilter.setSingleEntity(tenantAdminUserId);
+ AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery);
+
+ getWsClient().send(cmd3);
+
+ update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
+ Assert.assertEquals(2, update.getCmdId());
+ Assert.assertEquals(0, update.getCount());
+ }
+
+ @Test
+ public void testAlarmCountWsCmdWithDeviceType() throws Exception {
+ loginTenantAdmin();
+
+ DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
+ deviceTypeFilter.setDeviceTypes(List.of("default"));
+ AlarmCountQuery alarmCountQuery = new AlarmCountQuery(deviceTypeFilter);
+ AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
+
+ getWsClient().send(cmd1);
+
+ AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
+ Assert.assertEquals(1, update.getCmdId());
+ Assert.assertEquals(0, update.getCount());
+
+ getWsClient().registerWaitForUpdate();
+
+ Alarm alarm = new Alarm();
+ alarm.setOriginator(device.getId());
+ alarm.setType("TEST ALARM");
+ alarm.setSeverity(AlarmSeverity.WARNING);
+
+ alarm = doPost("/api/alarm", alarm, Alarm.class);
+
+ update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate());
+ Assert.assertEquals(1, update.getCmdId());
+ Assert.assertEquals(1, update.getCount());
+
+ deviceTypeFilter.setDeviceTypes(List.of("non-existing"));
+ AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery);
+
+ getWsClient().send(cmd3);
+
+ update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
+ Assert.assertEquals(3, update.getCmdId());
+ Assert.assertEquals(0, update.getCount());
+ }
+
@Test
public void testAlarmStatusWsCmd() throws Exception {
loginTenantAdmin();
@@ -372,17 +450,18 @@ public class WebsocketApiTest extends AbstractControllerTest {
doPost("/api/alarm", alarm2, Alarm.class);
- AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class);
+ AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
Assert.assertEquals(1, alarmStatusUpdate3.getCmdId());
Assert.assertTrue(alarmStatusUpdate3.isActive());
//change severity
+ getWsClient().registerWaitForUpdate();
alarm2.setSeverity(AlarmSeverity.MAJOR);
Alarm updatedAlarm = doPost("/api/alarm", alarm2, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
- AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class);
+ AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
Assert.assertEquals(1, alarmStatusUpdate4.getCmdId());
Assert.assertFalse(alarmStatusUpdate4.isActive());
diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
index 9212afa7c5..feac7adb04 100644
--- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
@@ -90,14 +90,12 @@ import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg;
import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
-import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
-import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
@@ -142,35 +140,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
installation();
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
+ edgeImitator.expectMessageAmount(25);
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);
edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class);
- edgeImitator.expectMessageAmount(26);
edgeImitator.connect();
- requestEdgeRuleChainMetadata();
-
verifyEdgeConnectionAndInitialData();
}
- private void requestEdgeRuleChainMetadata() throws Exception {
- RuleChainId rootRuleChainId = getEdgeRootRuleChainId();
- RuleChainMetadataRequestMsg.Builder builder = RuleChainMetadataRequestMsg.newBuilder()
- .setRuleChainIdMSB(rootRuleChainId.getId().getMostSignificantBits())
- .setRuleChainIdLSB(rootRuleChainId.getId().getLeastSignificantBits());
- testAutoGeneratedCodeByProtobuf(builder);
- UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder()
- .addRuleChainMetadataRequestMsg(builder.build());
- edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
- }
-
- private RuleChainId getEdgeRootRuleChainId() throws Exception {
- return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference>() {
- },
- new PageLink(100, 0, "Edge Root Rule Chain"),
- "EDGE")
- .getData().get(0).getId();
- }
-
@After
public void teardownEdgeTest() {
try {
@@ -213,6 +190,19 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class);
}
+ private RuleChainId getEdgeRootRuleChainId() throws Exception {
+ List edgeRuleChains = doGetTypedWithPageLink("/api/ruleChains?type={type}&",
+ new TypeReference>() {},
+ new PageLink(100, 0, "Edge Root Rule Chain"),
+ "EDGE").getData();
+ for (RuleChain edgeRuleChain : edgeRuleChains) {
+ if (edgeRuleChain.isRoot()) {
+ return edgeRuleChain.getId();
+ }
+ }
+ throw new RuntimeException("Root rule chain not found");
+ }
+
protected void extendDeviceProfileData(DeviceProfile deviceProfile) {
DeviceProfileData profileData = deviceProfile.getProfileData();
List alarms = new ArrayList<>();
@@ -255,8 +245,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(RuleChainUpdateMsg.class, 1);
UUID ruleChainUUID = validateRuleChains();
- // 1 from request message
- validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2);
+ // 1 from rule chain fetcher
+ validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1);
validateRuleChainMetadataUpdates(ruleChainUUID);
// 4 messages ('general', 'mail', 'connectivity', 'jwt')
@@ -438,12 +428,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
private void validateRuleChainMetadataUpdates(UUID expectedRuleChainUUID) {
- Optional ruleChainMetadataUpdateOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class);
- Assert.assertTrue(ruleChainMetadataUpdateOpt.isPresent());
- RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateOpt.get();
+ Optional ruleChainMetadataUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class);
+ Assert.assertTrue(ruleChainMetadataUpdateMsgOpt.isPresent());
+ RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType());
RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
- Assert.assertNotNull(ruleChainMetaData);
Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId());
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
index 79ec2f3d87..4d9840b936 100644
--- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
@@ -169,6 +169,7 @@ public class AssetEdgeTest extends AbstractEdgeTest {
public void testSendAssetToCloud() throws Exception {
Asset asset = buildAssetForUplinkMsg("Asset Edge 2");
+ // created asset on edge
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder();
assetUpdateMsgBuilder.setIdMSB(asset.getUuidId().getMostSignificantBits());
@@ -191,6 +192,32 @@ public class AssetEdgeTest extends AbstractEdgeTest {
Asset foundAsset = doGet("/api/asset/" + asset.getUuidId(), Asset.class);
Assert.assertNotNull(foundAsset);
Assert.assertEquals("Asset Edge 2", foundAsset.getName());
+
+ // update asset on edge
+ asset.setName("Asset Edge 2 Updated");
+
+ uplinkMsgBuilder = UplinkMsg.newBuilder();
+ assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder();
+ assetUpdateMsgBuilder.setIdMSB(asset.getUuidId().getMostSignificantBits());
+ assetUpdateMsgBuilder.setIdLSB(asset.getUuidId().getLeastSignificantBits());
+ assetUpdateMsgBuilder.setEntity(JacksonUtil.toString(asset));
+ assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
+ testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
+ uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());
+
+ testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+
+ Assert.assertTrue(edgeImitator.waitForResponses());
+
+ latestResponseMsg = edgeImitator.getLatestResponseMsg();
+ Assert.assertTrue(latestResponseMsg.getSuccess());
+
+ foundAsset = doGet("/api/asset/" + asset.getUuidId(), Asset.class);
+ Assert.assertNotNull(foundAsset);
+ Assert.assertEquals("Asset Edge 2 Updated", foundAsset.getName());
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
index b848178c58..bbf3d17f0d 100644
--- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
@@ -184,6 +184,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
+ // create dashboard on edge
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder();
dashboardUpdateMsgBuilder.setIdMSB(dashboard.getUuidId().getMostSignificantBits());
diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
index a6cd127097..da8e7563e0 100644
--- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
@@ -593,8 +593,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
@Test
public void testSendDeviceToCloud() throws Exception {
- Device deviceMsg = buildDeviceForUplinkMsg("Edge Device 2", "test");
+ String deviceName = "Edge Device 2";
+ Device deviceMsg = buildDeviceForUplinkMsg(deviceName, "test");
+ // create device on edge
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder();
deviceUpdateMsgBuilder.setIdMSB(deviceMsg.getUuidId().getMostSignificantBits());
@@ -609,7 +611,25 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
Device device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class);
Assert.assertNotNull(device);
- Assert.assertEquals("Edge Device 2", device.getName());
+ Assert.assertEquals(deviceName, device.getName());
+
+ // update device on edge
+ deviceMsg.setName(deviceName + " Updated");
+ uplinkMsgBuilder = UplinkMsg.newBuilder();
+ deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder();
+ deviceUpdateMsgBuilder.setIdMSB(deviceMsg.getUuidId().getMostSignificantBits());
+ deviceUpdateMsgBuilder.setIdLSB(deviceMsg.getUuidId().getLeastSignificantBits());
+ deviceUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceMsg));
+ deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
+ uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());
+
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+ Assert.assertTrue(edgeImitator.waitForResponses());
+
+ device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class);
+ Assert.assertNotNull(device);
+ Assert.assertEquals(deviceName + " Updated", device.getName());
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
index a5fea1694a..7c029e0471 100644
--- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
@@ -15,7 +15,7 @@
*/
package org.thingsboard.server.edge;
-import com.google.protobuf.AbstractMessage;
+import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
@@ -29,16 +29,17 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.service.DaoSqlTest;
-import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
+import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -74,8 +75,9 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
Assert.assertNotNull(ruleChainMetaData);
Assert.assertEquals(ruleChainMetaData.getRuleChainId(), savedRuleChain.getId());
-
- testRuleChainMetadataRequestMsg(savedRuleChain.getId());
+ for (RuleNode ruleNode : ruleChainMetaData.getNodes()) {
+ Assert.assertEquals(CONFIGURATION_VERSION, ruleNode.getConfigurationVersion());
+ }
// unassign rule chain from edge
edgeImitator.expectMessageAmount(1);
@@ -97,60 +99,62 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
}
@Test
- public void testSendRuleChainMetadataRequestToCloud() throws Exception {
- RuleChainId edgeRootRuleChainId = edge.getRootRuleChainId();
-
+ public void testRuleChainToCloud() throws Exception {
+ String ruleChainName = "Rule Chain Edge";
+ UUID uuid = Uuids.timeBased();
+
+ // create rule chain on edge
+ RuleChain edgeRuleChain = new RuleChain();
+ edgeRuleChain.setTenantId(tenantId);
+ edgeRuleChain.setId(new RuleChainId(uuid));
+ edgeRuleChain.setName(ruleChainName);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
- RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder();
- ruleChainMetadataRequestMsgBuilder.setRuleChainIdMSB(edgeRootRuleChainId.getId().getMostSignificantBits());
- ruleChainMetadataRequestMsgBuilder.setRuleChainIdLSB(edgeRootRuleChainId.getId().getLeastSignificantBits());
- testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder);
- uplinkMsgBuilder.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build());
+ RuleChainUpdateMsg.Builder ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder();
+ ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
+ ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
+ ruleChainUpdateMsgBuilder.setEntity(JacksonUtil.toString(edgeRuleChain));
+ ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
+ testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder);
+ uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
- edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+
Assert.assertTrue(edgeImitator.waitForResponses());
- Assert.assertTrue(edgeImitator.waitForMessages());
- AbstractMessage latestMessage = edgeImitator.getLatestMessage();
- Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg);
- RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage;
- RuleChainMetaData ruleChainMetadataMsg = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
- Assert.assertNotNull(ruleChainMetadataMsg);
- Assert.assertEquals(edgeRootRuleChainId, ruleChainMetadataMsg.getRuleChainId());
+ UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
+ Assert.assertTrue(latestResponseMsg.getSuccess());
- testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg);
- }
+ RuleChain ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class);
+ Assert.assertNotNull(ruleChain);
+ Assert.assertEquals("Rule Chain Edge", ruleChain.getName());
- private void testRuleChainMetadataRequestMsg(RuleChainId ruleChainId) throws Exception {
- RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder()
- .setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits())
- .setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits());
- testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder);
+ // update rule chain on edge
+ edgeRuleChain.setName(ruleChainName + " Updated");
+ uplinkMsgBuilder = UplinkMsg.newBuilder();
+ ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder();
+ ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
+ ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
+ ruleChainUpdateMsgBuilder.setEntity(JacksonUtil.toString(edgeRuleChain));
+ ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
+ testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder);
+ uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build());
- UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder()
- .addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
- edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+
Assert.assertTrue(edgeImitator.waitForResponses());
- Assert.assertTrue(edgeImitator.waitForMessages());
- AbstractMessage latestMessage = edgeImitator.getLatestMessage();
- Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg);
- RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage;
- RuleChainMetaData ruleChainMetadataMsg = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
- Assert.assertNotNull(ruleChainMetadataMsg);
- Assert.assertEquals(ruleChainId, ruleChainMetadataMsg.getRuleChainId());
+ latestResponseMsg = edgeImitator.getLatestResponseMsg();
+ Assert.assertTrue(latestResponseMsg.getSuccess());
- for (RuleNode ruleNode : ruleChainMetadataMsg.getNodes()) {
- Assert.assertEquals(CONFIGURATION_VERSION, ruleNode.getConfigurationVersion());
- }
+ ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class);
+ Assert.assertNotNull(ruleChain);
+ Assert.assertEquals(ruleChainName + " Updated", ruleChain.getName());
}
private RuleChainMetaData createRuleChainMetadata(RuleChain ruleChain) {
diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
index 17d0f3fe84..4a303e5253 100644
--- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
@@ -153,7 +153,7 @@ public class HashPartitionServiceTest {
for (int queueIndex = 0; queueIndex < queueCount; queueIndex++) {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId);
for (int partition = 0; partition < partitionCount; partition++) {
- ServiceInfo serviceInfo = partitionService.resolveByPartitionIdx(services, queueKey, partition, Collections.emptyMap());
+ ServiceInfo serviceInfo = partitionService.resolveByPartitionIdx(services, queueKey, partition, Collections.emptyMap()).get(0);
String serviceId = serviceInfo.getServiceId();
map.put(serviceId, map.get(serviceId) + 1);
}
@@ -308,7 +308,7 @@ public class HashPartitionServiceTest {
partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine));
verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TenantId.SYS_TENANT_ID);
- return event.getPartitionsMap().get(queueKey).size() == systemQueue.getPartitions();
+ return event.getNewPartitions().get(queueKey).size() == systemQueue.getPartitions();
});
Mockito.reset(applicationEventPublisher);
@@ -336,14 +336,14 @@ public class HashPartitionServiceTest {
// expecting event about no partitions for isolated queue key
verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId);
- return event.getPartitionsMap().get(queueKey).isEmpty();
+ return event.getNewPartitions().get(queueKey).isEmpty();
});
partitionService_dedicated.updateQueues(List.of(queueUpdateMsg));
partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine));
verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId);
- return event.getPartitionsMap().get(queueKey).size() == isolatedQueue.getPartitions();
+ return event.getNewPartitions().get(queueKey).size() == isolatedQueue.getPartitions();
});
@@ -361,7 +361,7 @@ public class HashPartitionServiceTest {
partitionService_dedicated.removeQueues(List.of(queueDeleteMsg));
verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId);
- return event.getPartitionsMap().get(queueKey).isEmpty();
+ return event.getNewPartitions().get(queueKey).isEmpty();
});
}
@@ -381,12 +381,12 @@ public class HashPartitionServiceTest {
Stream.concat(Stream.of(TenantId.SYS_TENANT_ID), Stream.generate(UUID::randomUUID).map(TenantId::new).limit(10)).forEach(tenantId -> {
List queues = Stream.generate(() -> RandomStringUtils.randomAlphabetic(10))
.map(queueName -> new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId))
- .limit(100).collect(Collectors.toList());
+ .limit(100).toList();
for (int partition = 0; partition < 10; partition++) {
- ServiceInfo expectedAssignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId), partition, Collections.emptyMap());
+ ServiceInfo expectedAssignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId), partition, Collections.emptyMap()).get(0);
for (QueueKey queueKey : queues) {
- ServiceInfo assignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, queueKey, partition, Collections.emptyMap());
+ ServiceInfo assignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, queueKey, partition, Collections.emptyMap()).get(0);
assertThat(assignedRuleEngine).as(queueKey + "[" + partition + "] should be assigned to " + expectedAssignedRuleEngine.getServiceId())
.isEqualTo(expectedAssignedRuleEngine);
}
@@ -434,6 +434,7 @@ public class HashPartitionServiceTest {
ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName);
ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge");
ReflectionTestUtils.setField(partitionService, "edgePartitions", 10);
+ ReflectionTestUtils.setField(partitionService, "edqsPartitions", 12);
partitionService.init();
partitionService.partitionsInit();
return partitionService;
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 2cced15f59..dee32e7d1f 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
@@ -90,7 +90,7 @@ public class ScriptCalculatedFieldStateTest {
state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry));
Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry);
- boolean stateUpdated = state.updateState(newArgs);
+ boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@@ -107,7 +107,7 @@ public class ScriptCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L);
Map newArgs = Map.of("assetHumidity", newArgEntry);
- boolean stateUpdated = state.updateState(newArgs);
+ boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
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 c06e835937..f0059e4f6f 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
@@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField
import org.thingsboard.server.common.data.id.AssetId;
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.kv.StringDataEntry;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
@@ -87,7 +88,7 @@ public class SimpleCalculatedFieldStateTest {
));
Map newArgs = Map.of("key3", key3ArgEntry);
- boolean stateUpdated = state.updateState(newArgs);
+ boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@@ -105,7 +106,7 @@ public class SimpleCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L);
Map newArgs = Map.of("key1", newArgEntry);
- boolean stateUpdated = state.updateState(newArgs);
+ boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry));
@@ -119,7 +120,7 @@ public class SimpleCalculatedFieldStateTest {
));
Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
- assertThatThrownBy(() -> state.updateState(newArgs))
+ assertThatThrownBy(() -> state.updateState(ctx, newArgs))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields.");
}
@@ -138,7 +139,7 @@ public class SimpleCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
- assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.0)));
+ assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49)));
}
@Test
@@ -154,6 +155,26 @@ public class SimpleCalculatedFieldStateTest {
.hasMessage("Argument 'key2' is not a number.");
}
+ @Test
+ void testPerformCalculationWhenDecimalsByDefault() throws ExecutionException, InterruptedException {
+ state.arguments = new HashMap<>(Map.of(
+ "key1", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("key1", 11.3456), 145L),
+ "key2", new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("key2", 15.1), 165L),
+ "key3", new SingleValueArgumentEntry(System.currentTimeMillis() - 3, new DoubleDataEntry("key3", 23.1), 184L)
+ ));
+
+ Output output = getCalculatedFieldConfig().getOutput();
+ output.setDecimalsByDefault(3);
+ ctx.setOutput(output);
+
+ CalculatedFieldResult result = state.performCalculation(ctx).get();
+
+ assertThat(result).isNotNull();
+ assertThat(result.getType()).isEqualTo(output.getType());
+ assertThat(result.getScope()).isEqualTo(output.getScope());
+ assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.546)));
+ }
+
@Test
void testIsReadyWhenNotAllArgPresent() {
assertThat(state.isReady()).isFalse();
@@ -219,6 +240,7 @@ public class SimpleCalculatedFieldStateTest {
output.setName("output");
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE);
+ output.setDecimalsByDefault(0);
config.setOutput(output);
diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
index e83e30663d..2c48ed9167 100644
--- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
@@ -71,6 +71,6 @@ public class SingleValueArgumentEntryTest {
@Test
void testUpdateEntryWhenValueWasNotChanged() {
- assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 237L))).isFalse();
+ assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue();
}
}
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java
new file mode 100644
index 0000000000..50c80d08c7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java
@@ -0,0 +1,128 @@
+/**
+ * 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.service.entitiy;
+
+import com.google.common.collect.Lists;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.query.EntityCountQuery;
+import org.thingsboard.server.common.data.query.EntityData;
+import org.thingsboard.server.common.data.query.EntityDataQuery;
+import org.thingsboard.server.common.data.query.EntityKeyType;
+import org.thingsboard.server.common.data.query.RelationsQueryFilter;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
+import org.thingsboard.server.common.msg.edqs.EdqsApiService;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.edqs.util.EdqsRocksDb;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.awaitility.Awaitility.await;
+
+@DaoSqlTest
+@TestPropertySource(properties = {
+ "queue.edqs.sync.enabled=true",
+ "queue.edqs.api.supported=true",
+ "queue.edqs.api.auto_enable=true",
+ "queue.edqs.mode=local"
+})
+public class EdqsEntityServiceTest extends EntityServiceTest {
+
+ @Autowired
+ private EdqsApiService edqsApiService;
+
+ @MockBean
+ private EdqsRocksDb edqsRocksDb;
+
+ @Before
+ public void beforeEach() {
+ await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsApiService.isEnabled());
+ }
+
+ // sql implementation has a bug with data duplication, edqs implementation returns correct value
+ @Override
+ @Test
+ public void testCountHierarchicalEntitiesByMultiRootQuery() throws InterruptedException {
+ List buildings = new ArrayList<>();
+ List apartments = new ArrayList<>();
+ Map> entityNameByTypeMap = new HashMap<>();
+ Map childParentRelationMap = new HashMap<>();
+ createMultiRootHierarchy(buildings, apartments, entityNameByTypeMap, childParentRelationMap);
+
+ RelationsQueryFilter filter = new RelationsQueryFilter();
+ filter.setMultiRoot(true);
+ filter.setMultiRootEntitiesType(EntityType.ASSET);
+ filter.setMultiRootEntityIds(buildings.stream().map(IdBased::getId).map(d -> d.getId().toString()).collect(Collectors.toSet()));
+ filter.setDirection(EntitySearchDirection.FROM);
+
+ EntityCountQuery countQuery = new EntityCountQuery(filter);
+ countByQueryAndCheck(countQuery, 63);
+
+ filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("AptToHeat", Collections.singletonList(EntityType.DEVICE))));
+ countByQueryAndCheck(countQuery, 27);
+
+ filter.setMultiRootEntitiesType(EntityType.ASSET);
+ filter.setMultiRootEntityIds(apartments.stream().map(IdBased::getId).map(d -> d.getId().toString()).collect(Collectors.toSet()));
+ filter.setDirection(EntitySearchDirection.TO);
+ filter.setFilters(Lists.newArrayList(
+ new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)),
+ new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE))));
+ countByQueryAndCheck(countQuery, 3);
+
+ deviceService.deleteDevicesByTenantId(tenantId);
+ assetService.deleteAssetsByTenantId(tenantId);
+ }
+
+ @Override
+ protected PageData findByQueryAndCheck(CustomerId customerId, EntityDataQuery query, long expectedResultSize) {
+ return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findByQuery(customerId, query),
+ result -> result.getTotalElements() == expectedResultSize);
+ }
+
+ @Override
+ protected List findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List expectedTelemetries) {
+ return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findEntitiesTelemetry(query, entityKeyType, key, expectedTelemetries),
+ loadedEntities -> loadedEntities.stream().map(entityData -> entityData.getLatest().get(entityKeyType).get(key).getValue()).toList().containsAll(expectedTelemetries));
+ }
+
+ @Override
+ protected long countByQueryAndCheck(EntityCountQuery countQuery, int expectedResult) {
+ return countByQueryAndCheck(new CustomerId(CustomerId.NULL_UUID), countQuery, expectedResult);
+ }
+
+ @Override
+ protected long countByQueryAndCheck(CustomerId customerId, EntityCountQuery query, int expectedResult) {
+ return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(customerId, query),
+ result -> result == expectedResult);
+ }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
similarity index 80%
rename from dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
rename to application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
index 77f9a6bc23..18687cedba 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
@@ -13,22 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao.service;
+package org.thingsboard.server.service.entitiy;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.ResultSetExtractor;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
@@ -37,6 +40,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -47,6 +51,7 @@ import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
+import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.ApiUsageStateFilter;
import org.thingsboard.server.common.data.query.AssetSearchQueryFilter;
@@ -64,6 +69,7 @@ import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityNameFilter;
+import org.thingsboard.server.common.data.query.EntityViewTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@@ -76,17 +82,22 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardDao;
+import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewDao;
+import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.relation.RelationRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
@@ -106,13 +117,12 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertEquals;
import static org.thingsboard.server.common.data.query.EntityKeyType.ATTRIBUTE;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
@Slf4j
@DaoSqlTest
-public class EntityServiceTest extends AbstractServiceTest {
+public class EntityServiceTest extends AbstractControllerTest {
static final int ENTITY_COUNT = 5;
public static final String TEST_CUSTOMER_NAME = "Test";
@@ -120,6 +130,12 @@ public class EntityServiceTest extends AbstractServiceTest {
@Autowired
AssetService assetService;
@Autowired
+ AssetProfileService assetProfileService;
+ @Autowired
+ DashboardService dashboardService;
+ @Autowired
+ EntityViewService entityViewService;
+ @Autowired
UserService userService;
@Autowired
AttributesService attributesService;
@@ -158,7 +174,7 @@ public class EntityServiceTest extends AbstractServiceTest {
}
@Test
- public void testCountEntitiesByQuery() throws InterruptedException {
+ public void testCountEntitiesByQuery() {
List devices = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Device device = new Device();
@@ -174,33 +190,26 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setDeviceNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter);
-
- long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(97, count);
+ countByQueryAndCheck(countQuery, 97);
filter.setDeviceTypes(List.of("unknown"));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(0, count);
+ countByQueryAndCheck(countQuery, 0);
filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("Device1");
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(11, count);
+ countByQueryAndCheck(countQuery, 11);
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.DEVICE);
entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList()));
countQuery = new EntityCountQuery(entityListFilter);
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(97, count);
+ countByQueryAndCheck(countQuery, 97);
deviceService.deleteDevicesByTenantId(tenantId);
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(0, count);
+ countByQueryAndCheck(countQuery, 0);
}
-
@Test
public void testCountHierarchicalEntitiesByQuery() throws InterruptedException {
List assets = new ArrayList<>();
@@ -212,19 +221,15 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setDirection(EntitySearchDirection.FROM);
EntityCountQuery countQuery = new EntityCountQuery(filter);
-
- long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(31, count); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31)
+ countByQueryAndCheck(countQuery, 31); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31)
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(25, count);
+ countByQueryAndCheck(countQuery, 25);
filter.setRootEntity(devices.get(0).getId());
filter.setDirection(EntitySearchDirection.TO);
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT))));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(1, count);
+ countByQueryAndCheck(countQuery, 1);
DeviceSearchQueryFilter filter2 = new DeviceSearchQueryFilter();
filter2.setRootEntity(tenantId);
@@ -232,18 +237,14 @@ public class EntityServiceTest extends AbstractServiceTest {
filter2.setRelationType("Contains");
countQuery = new EntityCountQuery(filter2);
-
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(25, count);
+ countByQueryAndCheck(countQuery, 25);
filter2.setDeviceTypes(Arrays.asList("default0", "default1"));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(10, count);
+ countByQueryAndCheck(countQuery, 10);
filter2.setRootEntity(devices.get(0).getId());
filter2.setDirection(EntitySearchDirection.TO);
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(0, count);
+ countByQueryAndCheck(countQuery, 0);
AssetSearchQueryFilter filter3 = new AssetSearchQueryFilter();
filter3.setRootEntity(tenantId);
@@ -251,18 +252,14 @@ public class EntityServiceTest extends AbstractServiceTest {
filter3.setRelationType("Manages");
countQuery = new EntityCountQuery(filter3);
-
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(5, count);
+ countByQueryAndCheck(countQuery, 5);
filter3.setAssetTypes(Arrays.asList("type0", "type1"));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(2, count);
+ countByQueryAndCheck(countQuery, 2);
filter3.setRootEntity(devices.get(0).getId());
filter3.setDirection(EntitySearchDirection.TO);
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(0, count);
+ countByQueryAndCheck(countQuery, 0);
}
@Test
@@ -279,11 +276,12 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- PageData entityDataByQuery = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
+ PageData entityDataByQuery = findByQueryAndCheck(query, 5);
List data = entityDataByQuery.getData();
Assert.assertEquals(data.size(), 5);
data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone")));
+ countByQueryAndCheck(query, 5);
}
private void createTestUserRelations(TenantId tenantId, List users) {
@@ -313,30 +311,24 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setEdgeNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter);
-
- long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(97, count);
+ countByQueryAndCheck(countQuery, 97);
filter.setEdgeTypes(List.of("unknown"));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(0, count);
+ countByQueryAndCheck(countQuery, 0);
filter.setEdgeTypes(List.of("default"));
filter.setEdgeNameFilter("Edge1");
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(11, count);
+ countByQueryAndCheck(countQuery, 11);
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.EDGE);
entityListFilter.setEntityList(edges.stream().map(Edge::getId).map(EdgeId::toString).collect(Collectors.toList()));
countQuery = new EntityCountQuery(entityListFilter);
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(97, count);
+ countByQueryAndCheck(countQuery, 97);
edgeService.deleteEdgesByTenantId(tenantId);
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(0, count);
+ countByQueryAndCheck(countQuery, 0);
}
@Test
@@ -361,13 +353,10 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setRelationType("Manages");
EntityCountQuery countQuery = new EntityCountQuery(filter);
-
- long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(5, count);
+ countByQueryAndCheck(countQuery, 5);
filter.setEdgeTypes(Arrays.asList("type0", "type1"));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(2, count);
+ countByQueryAndCheck(countQuery, 2);
}
private Edge createEdge(int i, String type) {
@@ -424,19 +413,10 @@ public class EntityServiceTest extends AbstractServiceTest {
List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
- EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- List loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
- Assert.assertEquals(25, loadedEntities.size());
- List loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+
+ EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceTemperatures);
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter();
@@ -447,23 +427,10 @@ public class EntityServiceTest extends AbstractServiceTest {
highTemperatureFilter.setPredicate(predicate);
List keyFilters = Collections.singletonList(highTemperatureFilter);
- query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
-
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
-
- loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
- Assert.assertEquals(highTemperatures.size(), loadedEntities.size());
-
- List loadedHighTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
- Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
+ query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceHighTemperatures);
deviceService.deleteDevicesByTenantId(tenantId);
}
@@ -483,13 +450,10 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setDirection(EntitySearchDirection.FROM);
EntityCountQuery countQuery = new EntityCountQuery(filter);
-
- long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(63, count);
+ countByQueryAndCheck(countQuery, 63);
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("AptToHeat", Collections.singletonList(EntityType.DEVICE))));
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(27, count);
+ countByQueryAndCheck(countQuery, 27);
filter.setMultiRootEntitiesType(EntityType.ASSET);
filter.setMultiRootEntityIds(apartments.stream().map(IdBased::getId).map(d -> d.getId().toString()).collect(Collectors.toSet()));
@@ -497,13 +461,10 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setFilters(Lists.newArrayList(
new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)),
new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE))));
-
- count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
- Assert.assertEquals(9, count);
+ countByQueryAndCheck(countQuery, 9);
deviceService.deleteDevicesByTenantId(tenantId);
assetService.deleteAssetsByTenantId(tenantId);
-
}
@Test
@@ -539,15 +500,6 @@ public class EntityServiceTest extends AbstractServiceTest {
onlineStatusFilter.setPredicate(predicate);
List keyFilters = Collections.singletonList(onlineStatusFilter);
- EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- List loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
-
long expectedEntitiesCnt = entityNameByTypeMap.entrySet()
.stream()
.filter(e -> !e.getKey().equals("building"))
@@ -555,6 +507,14 @@ public class EntityServiceTest extends AbstractServiceTest {
.map(Map.Entry::getValue)
.filter(e -> StringUtils.endsWith(e, "_1"))
.count();
+ EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
+ PageData data = findByQueryAndCheck(query, expectedEntitiesCnt);
+ List loadedEntities = new ArrayList<>(data.getData());
+ while (data.hasNext()) {
+ query = query.next();
+ data = findByQuery(query);
+ loadedEntities.addAll(data.getData());
+ }
Assert.assertEquals(expectedEntitiesCnt, loadedEntities.size());
Map actualRelations = new HashMap<>();
@@ -603,20 +563,12 @@ public class EntityServiceTest extends AbstractServiceTest {
List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
+ List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
+
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- List loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
- Assert.assertEquals(25, loadedEntities.size());
+ List loadedEntities = findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceTemperatures);
+
loadedEntities.forEach(entity -> Assert.assertTrue(devices.stream().map(Device::getId).collect(Collectors.toSet()).contains(entity.getEntityId())));
- List loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter();
@@ -629,21 +581,8 @@ public class EntityServiceTest extends AbstractServiceTest {
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
-
- loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
- Assert.assertEquals(highTemperatures.size(), loadedEntities.size());
-
- List loadedHighTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "temperature", deviceHighTemperatures);
deviceService.deleteDevicesByTenantId(tenantId);
}
@@ -677,18 +616,9 @@ public class EntityServiceTest extends AbstractServiceTest {
List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "consumption"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- List loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
- Assert.assertEquals(5, loadedEntities.size());
- List loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("consumption").getValue()).collect(Collectors.toList());
+
List deviceTemperatures = consumptions.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "consumption", deviceTemperatures);
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter();
@@ -701,21 +631,8 @@ public class EntityServiceTest extends AbstractServiceTest {
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
-
- loadedEntities = new ArrayList<>(data.getData());
- while (data.hasNext()) {
- query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities.addAll(data.getData());
- }
- Assert.assertEquals(highConsumptions.size(), loadedEntities.size());
-
- List loadedHighTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("consumption").getValue()).collect(Collectors.toList());
List deviceHighTemperatures = highConsumptions.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.ATTRIBUTE, "consumption", deviceHighTemperatures);
deviceService.deleteDevicesByTenantId(tenantId);
}
@@ -897,9 +814,7 @@ public class EntityServiceTest extends AbstractServiceTest {
List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
-
- Assert.assertEquals(97, data.getTotalElements());
+ PageData data = findByQueryAndCheck(query, 97);
Assert.assertEquals(10, data.getTotalPages());
Assert.assertTrue(data.hasNext());
Assert.assertEquals(10, data.getData().size());
@@ -907,7 +822,7 @@ public class EntityServiceTest extends AbstractServiceTest {
List loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) {
query = query.next();
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
+ data = findByQuery(query);
loadedEntities.addAll(data.getData());
}
Assert.assertEquals(97, loadedEntities.size());
@@ -932,7 +847,7 @@ public class EntityServiceTest extends AbstractServiceTest {
pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
+ data = findByQuery(query);
Assert.assertEquals(11, data.getTotalElements());
Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue());
@@ -946,11 +861,12 @@ public class EntityServiceTest extends AbstractServiceTest {
devices.get(1).setLabel(null);
devices.forEach(deviceService::saveDevice);
+ // FIXME (for Dasha, plz investigate):
+ // this and other tests below submit an empty value to a KEY FILTER, this is not "search text".
+ // why are we supposed to ignore it and return all devices? maybe it's a bug?
String searchQuery = "";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.EQUAL, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
}
@Test
@@ -962,9 +878,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = devices.get(2).getLabel();
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_EQUAL, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size() - 1, result.getTotalElements());
+ findByQueryAndCheck(query, devices.size() - 1);
}
@Test
@@ -977,8 +891,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = "";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_EQUAL, searchQuery);
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
}
@Test
@@ -990,9 +903,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = "";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.STARTS_WITH, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
}
@Test
@@ -1004,9 +915,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = "";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.ENDS_WITH, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
}
@Test
@@ -1018,9 +927,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = "";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.CONTAINS, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
}
@Test
@@ -1032,9 +939,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = "label-";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_CONTAINS, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(2, result.getTotalElements());
+ findByQueryAndCheck(query, 2);
}
@Test
@@ -1046,9 +951,7 @@ public class EntityServiceTest extends AbstractServiceTest {
String searchQuery = "";
EntityDataQuery query = createDeviceSearchQuery("label", StringOperation.NOT_CONTAINS, searchQuery);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
}
@Test
@@ -1072,34 +975,27 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
-
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setEntityNameFilter("Device%");
-
- result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setEntityNameFilter("%Device%");
-
- result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setEntityNameFilter("%Device");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
public void testFindEntityDataByQuery_filter_entity_name_ends_with() {
List devices = new ArrayList<>();
+ String suffixes = RandomStringUtils.randomAlphanumeric(5);
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setTenantId(tenantId);
- device.setName("Device " + i + " test");
+ device.setName("Device " + i + suffixes);
device.setType("default");
devices.add(device);
}
@@ -1108,29 +1004,21 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityNameFilter deviceTypeFilter = new EntityNameFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
- deviceTypeFilter.setEntityNameFilter("%test");
+ deviceTypeFilter.setEntityNameFilter("%" + suffixes);
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
+ findByQueryAndCheck(query, devices.size());
- PageData result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ deviceTypeFilter.setEntityNameFilter("%" + suffixes + "%");
+ findByQueryAndCheck(query, devices.size());
- deviceTypeFilter.setEntityNameFilter("%test%");
-
- result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
-
- deviceTypeFilter.setEntityNameFilter("test%");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ deviceTypeFilter.setEntityNameFilter(suffixes + "%");
+ findByQueryAndCheck(query, 0);
- deviceTypeFilter.setEntityNameFilter("test");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ deviceTypeFilter.setEntityNameFilter(suffixes);
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1154,19 +1042,13 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(deviceTypeFilter, pageLink, null, null, null);
-
- PageData