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/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index de3864df9f..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
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 e42ebb593e..9ab019097b 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
@@ -134,14 +134,20 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(CalculatedFieldEntityDeleteMsg msg) {
log.info("[{}] Processing CF entity delete msg.", msg.getEntityId());
if (this.entityId.equals(msg.getEntityId())) {
- MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback());
- states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback));
- ctx.stop(ctx.getSelf());
+ if (states.isEmpty()) {
+ msg.getCallback().onSuccess();
+ } else {
+ MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback());
+ states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback));
+ ctx.stop(ctx.getSelf());
+ }
} else {
var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var state = states.remove(cfId);
if (state != null) {
cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback());
+ } else {
+ msg.getCallback().onSuccess();
}
}
}
@@ -242,7 +248,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);
@@ -301,7 +307,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
}
- 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
@@ -314,6 +320,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..1418821081 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;
@@ -202,7 +199,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (fieldsCount > 0) {
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
var entityId = msg.getEntityId();
- oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), callback));
+ oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback));
newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback));
} else {
callback.onSuccess();
@@ -309,7 +306,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (!entityIds.isEmpty()) {
//TODO: no need to do this if we cache all created actors and know which one belong to us;
var multiCallback = new MultipleTbCallback(entityIds.size(), callback);
- entityIds.forEach(id -> deleteCfForEntity(entityId, cfId, multiCallback));
+ entityIds.forEach(id -> deleteCfForEntity(id, cfId, multiCallback));
} else {
callback.onSuccess();
}
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/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
index ff4de0ce0c..1a3bdce1f6 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
@@ -160,7 +161,12 @@ public class TenantProfileController extends BaseController {
" \"rpcTtlDays\": 0,\n" +
" \"queueStatsTtlDays\": 0,\n" +
" \"ruleEngineExceptionsTtlDays\": 0,\n" +
- " \"warnThreshold\": 0\n" +
+ " \"warnThreshold\": 0,\n" +
+ " \"maxCalculatedFieldsPerEntity\": 5,\n" +
+ " \"maxArgumentsPerCF\": 10,\n" +
+ " \"maxDataPointsPerRollingArg\": 1000,\n" +
+ " \"maxStateSizeInKBytes\": 32,\n" +
+ " \"maxSingleValueArgumentSizeInKBytes\": 2" +
" }\n" +
" },\n" +
" \"default\": false\n" +
@@ -172,7 +178,7 @@ public class TenantProfileController extends BaseController {
@RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
@ResponseBody
public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.")
- @RequestBody TenantProfile tenantProfile) throws ThingsboardException {
+ @Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException {
TenantProfile oldProfile;
if (tenantProfile.getId() == null) {
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE);
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..27bc0120c4 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 -> {
@@ -273,7 +273,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
long startTs = currentTime - timeWindow;
long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
- int limit = argument.getLimit() == 0 ? (int) maxDataPoints : argument.getLimit();
+ int argumentLimit = argument.getLimit();
+ int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit();
ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE);
ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));
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/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
index a237f3d022..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);
}
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/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 03bd7b4e66..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
@@ -77,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) {
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 7219179b2c..50989ea5a0 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()));
@@ -723,10 +748,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..cc476d377d 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);
+
+ if (inactivityTimeoutAttribute.isPresent()) {
+ List result = new ArrayList<>(activityTimeseries.size() + 1);
+ result.addAll(activityTimeseries);
+ result.add(inactivityTimeoutAttribute.get());
+ return result;
+ } else {
+ return activityTimeseries;
+ }
+ }, 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 +698,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService 0 ? inactivityTimeout : defaultInactivityTimeoutMs)
.build();
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName());
@@ -761,12 +769,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 23adacfc2e..2cdee7b7d0 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
@@ -161,9 +161,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();
}
@@ -171,9 +168,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/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/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index 2bdbd917ab..36cd5516a8 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
@@ -259,8 +259,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
.strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
.callback(new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable Void tmp) {
- }
+ public void onSuccess(@Nullable Void tmp) {}
@Override
public void onFailure(Throwable t) {
@@ -324,6 +323,10 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
addMainCallback(saveFuture, result -> callback.onSuccess(null), callback::onFailure);
}
+ private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess) {
+ addMainCallback(saveFuture, onSuccess, null);
+ }
+
private void addMainCallback(ListenableFuture saveFuture, Consumer onSuccess, Consumer onFailure) {
DonAsynchron.withCallback(saveFuture, onSuccess, onFailure, tsCallBackExecutor);
}
@@ -345,13 +348,12 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
@Override
- public void onFailure(Throwable t) {
- }
+ public void onFailure(Throwable t) {}
};
}
private FutureCallback getCalculatedFieldCallback(FutureCallback> originalCallback, List keys) {
- return new FutureCallback() {
+ return new FutureCallback<>() {
@Override
public void onSuccess(Void unused) {
originalCallback.onSuccess(keys);
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 53258a4296..01d7e69ba7 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -858,6 +858,7 @@ audit-log:
"edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
+ "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
sink:
# Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}"
@@ -1599,6 +1600,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
@@ -1632,6 +1643,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}"
@@ -1707,6 +1724,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}"
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/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 e179e8e706..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
@@ -88,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(
@@ -106,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));
@@ -120,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.");
}
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 result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setEntityNameFilter("test%");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
deviceTypeFilter.setEntityNameFilter("%test");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1190,24 +1072,16 @@ 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.setDeviceNameFilter("Device%");
-
- result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setDeviceNameFilter("%Device%");
-
- result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setDeviceNameFilter("%Device");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1233,31 +1107,12 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataQuery query = new EntityDataQuery(singleEntityFilter, pageLink, entityFields, null, null);
- PageData result = searchEntities(query);
- assertEquals(1, result.getTotalElements());
+ PageData result = findByQueryAndCheck(query, 1);
String deviceName = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
assertThat(deviceName).isEqualTo(devices.get(0).getName());
}
- @Test
- public void testFindEntitiesByApiUsageStateFilter() {
- apiUsageStateService.createDefaultApiUsageState(tenantId, customerId);
- ApiUsageStateFilter apiUsageStateFilter = new ApiUsageStateFilter();
- apiUsageStateFilter.setCustomerId(customerId);
-
- List entityFields = List.of(
- new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
- );
-
- EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
- EntityDataQuery query = new EntityDataQuery(apiUsageStateFilter, pageLink, entityFields, null, null);
- PageData result = searchEntities(query);
- assertEquals(1, result.getTotalElements());
- String name = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
- assertThat(name).isEqualTo(TEST_CUSTOMER_NAME);
- }
-
@Test
public void testFindEntitiesByRelationEntityTypeFilter() {
Customer customer = new Customer();
@@ -1313,11 +1168,8 @@ public class EntityServiceTest extends AbstractServiceTest {
filter.setRootEntity(asset.getId());
EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString);
- PageData relationsResult = entityService.findEntityDataByQuery(tenantId, customer.getId(), query);
- long relationsResultCnt = entityService.countEntitiesByQuery(tenantId, customer.getId(), query);
-
- Assert.assertEquals(relationsCnt, relationsResult.getData().size());
- Assert.assertEquals(relationsCnt, relationsResultCnt);
+ findByQueryAndCheck(customer.getId(), query, relationsCnt);
+ countByQueryAndCheck(customer.getId(), query, relationsCnt);
}
}
@@ -1342,24 +1194,16 @@ 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.setDeviceNameFilter("%test%");
-
- result = searchEntities(query);
- assertEquals(devices.size(), result.getTotalElements());
+ findByQueryAndCheck(query, devices.size());
deviceTypeFilter.setDeviceNameFilter("test%");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
deviceTypeFilter.setDeviceNameFilter("test");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1383,19 +1227,13 @@ 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.setDeviceNameFilter("test%");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
deviceTypeFilter.setDeviceNameFilter("%test");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1419,24 +1257,16 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null);
-
- PageData result = searchEntities(query);
- assertEquals(assets.size(), result.getTotalElements());
+ findByQueryAndCheck(query, assets.size());
assetTypeFilter.setAssetNameFilter("Asset%");
-
- result = searchEntities(query);
- assertEquals(assets.size(), result.getTotalElements());
+ findByQueryAndCheck(query, assets.size());
assetTypeFilter.setAssetNameFilter("%Asset%");
-
- result = searchEntities(query);
- assertEquals(assets.size(), result.getTotalElements());
+ findByQueryAndCheck(query, assets.size());
assetTypeFilter.setAssetNameFilter("%Asset");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1460,24 +1290,16 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null);
-
- PageData result = searchEntities(query);
- assertEquals(assets.size(), result.getTotalElements());
+ findByQueryAndCheck(query, assets.size());
assetTypeFilter.setAssetNameFilter("%test%");
-
- result = searchEntities(query);
- assertEquals(assets.size(), result.getTotalElements());
+ findByQueryAndCheck(query, assets.size());
assetTypeFilter.setAssetNameFilter("test%");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
assetTypeFilter.setAssetNameFilter("test");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
}
@Test
@@ -1489,6 +1311,7 @@ public class EntityServiceTest extends AbstractServiceTest {
asset.setTenantId(tenantId);
asset.setName("Asset test" + i);
asset.setType("default");
+ asset.setAssetProfileId(assetProfileService.findDefaultAssetProfile(tenantId).getId());
assets.add(asset);
}
@@ -1501,25 +1324,105 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(assetTypeFilter, pageLink, null, null, null);
-
- PageData result = searchEntities(query);
- assertEquals(assets.size(), result.getTotalElements());
+ findByQueryAndCheck(query, assets.size());
assetTypeFilter.setAssetNameFilter("test%");
-
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ findByQueryAndCheck(query, 0);
assetTypeFilter.setAssetNameFilter("%test");
+ findByQueryAndCheck(query, 0);
+ }
+
+ @Test
+ public void testFindEntitiesBySingleEntityFilter_customer() {
+ List customerDevices = new ArrayList<>();
+ List tenantDevices = new ArrayList<>();
+
+ for (int i = 0; i < 3; i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setCustomerId(customerId);
+ device.setName("Device test" + i);
+ device.setType("default");
+ Device saved = deviceService.saveDevice(device);
+ customerDevices.add(saved);
+ }
+
+ for (int i = 0; i < 3; i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("Tenant test device" + i);
+ device.setType("default");
+ tenantDevices.add(deviceService.saveDevice(device));
+ }
+
+ SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
+ singleEntityFilter.setSingleEntity(customerDevices.get(0).getId());
+ List entityFields = List.of(
+ new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
+ );
+ EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
+ EntityDataQuery query = new EntityDataQuery(singleEntityFilter, pageLink, entityFields, null, null);
- result = searchEntities(query);
- assertEquals(0, result.getTotalElements());
+ PageData result = findByQueryAndCheck(query, 1);
+ String deviceName = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
+ assertThat(deviceName).isEqualTo(customerDevices.get(0).getName());
+
+ // find by customer user with generic permission
+ PageData customerResults = findByQueryAndCheck(customerId, query, 1);
+
+ String cutomerDeviceName = customerResults.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
+ assertThat(cutomerDeviceName).isEqualTo(customerDevices.get(0).getName());
+
+ // try to find tenant device by customer user
+ SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter();
+ tenantDeviceFilter.setSingleEntity(tenantDevices.get(0).getId());
+ EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null);
+ findByQueryAndCheck(customerId, customerQuery2, 0);
}
- private PageData searchEntities(EntityDataQuery query) {
- return entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
+ private List getResultDeviceIds(PageData result) {
+ return result.getData().stream().map(entityData -> (DeviceId) entityData.getEntityId()).collect(Collectors.toList());
}
+ private Device createDevice(CustomerId customerId) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setCustomerId(customerId);
+ device.setName("Device test " + RandomStringUtils.randomAlphabetic(5));
+ device.setType("default");
+ return device;
+ }
+
+ @Test
+ public void testFindEntitiesByApiUsageStateFilter() {
+ ApiUsageStateFilter apiUsageStateFilter = new ApiUsageStateFilter();
+
+ List entityFields = List.of(
+ new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
+ );
+
+ EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
+ EntityDataQuery query = new EntityDataQuery(apiUsageStateFilter, pageLink, entityFields, null, null);
+ PageData result = findByQueryAndCheck(query, 1);
+ String name = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
+ assertThat(name).isEqualTo(TEST_TENANT_NAME);
+
+ // find by customer user with generic permissions
+ apiUsageStateService.createDefaultApiUsageState(tenantId, customerId);
+ PageData customerResult = findByQueryAndCheck(customerId, query, 1);
+
+ String customerResultName = customerResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
+ assertThat(customerResultName).isEqualTo(TEST_CUSTOMER_NAME);
+
+ // find by tenant user with customerId filter
+ apiUsageStateFilter.setCustomerId(customerId);
+ PageData tenantResult = findByQueryAndCheck(query, 1);
+ String tenantResultName = tenantResult.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
+ assertThat(tenantResultName).isEqualTo(TEST_CUSTOMER_NAME);
+ }
+
+
private EntityDataQuery createDeviceSearchQuery(String deviceField, StringOperation operation, String searchQuery) {
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceTypes(List.of("default"));
@@ -1598,45 +1501,15 @@ public class EntityServiceTest extends AbstractServiceTest {
for (EntityKeyType currentAttributeKeyType : attributesEntityTypes) {
List latestValues = Collections.singletonList(new EntityKey(currentAttributeKeyType, "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(67, loadedEntities.size());
- List loadedTemperatures = new ArrayList<>();
- for (Device device : devices) {
- loadedTemperatures.add(loadedEntities.stream().filter(entityData -> entityData.getEntityId().equals(device.getId())).findFirst().orElse(null)
- .getLatest().get(currentAttributeKeyType).get("temperature").getValue());
- }
- List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).toList();
+ findByQueryAndCheckTelemetry(query, currentAttributeKeyType, "temperature", deviceTemperatures);
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = createNumericKeyFilter("temperature", currentAttributeKeyType, NumericFilterPredicate.NumericOperation.GREATER, 45);
List keyFiltersHighTemperature = Collections.singletonList(highTemperatureFilter);
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersHighTemperature);
-
- 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(currentAttributeKeyType).get("temperature").getValue()).collect(Collectors.toList());
- List deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
-
+ findByQueryAndCheckTelemetry(query, currentAttributeKeyType, "temperature", highTemperatures.stream().map(Object::toString).toList());
}
deviceService.deleteDevicesByTenantId(tenantId);
}
@@ -1716,89 +1589,48 @@ public class EntityServiceTest extends AbstractServiceTest {
List keyFiltersNotEqualTemperature = Collections.singletonList(notEqualTemperatureFilter);
//Greater Operation
+ List deviceTemperatures = greaterTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterTemperature);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- List loadedEntities = getLoadedEntities(data, query);
- Assert.assertEquals(greaterTemperatures.size(), loadedEntities.size());
- List loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- List deviceTemperatures = greaterTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures);
//Greater or equal Operation
+ deviceTemperatures = greaterOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersGreaterOrEqualTemperature);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities = getLoadedEntities(data, query);
- Assert.assertEquals(greaterOrEqualTemperatures.size(), loadedEntities.size());
-
- loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- deviceTemperatures = greaterOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures);
//Less Operation
+ deviceTemperatures = lessTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessTemperature);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities = getLoadedEntities(data, query);
- Assert.assertEquals(lessTemperatures.size(), loadedEntities.size());
-
- loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- deviceTemperatures = lessTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures);
//Less or equal Operation
+ deviceTemperatures = lessOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersLessOrEqualTemperature);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities = getLoadedEntities(data, query);
- Assert.assertEquals(lessOrEqualTemperatures.size(), loadedEntities.size());
-
- loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- deviceTemperatures = lessOrEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures);
//Equal Operation
+ deviceTemperatures = equalTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualTemperature);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities = getLoadedEntities(data, query);
- Assert.assertEquals(equalTemperatures.size(), loadedEntities.size());
-
- loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- deviceTemperatures = equalTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures);
//Not equal Operation
+ deviceTemperatures = notEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersNotEqualTemperature);
- data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
- loadedEntities = getLoadedEntities(data, query);
- Assert.assertEquals(notEqualTemperatures.size(), loadedEntities.size());
-
- loadedTemperatures = loadedEntities.stream().map(entityData ->
- entityData.getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("temperature").getValue()).collect(Collectors.toList());
- deviceTemperatures = notEqualTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
-
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
-
+ findByQueryAndCheckTelemetry(query, EntityKeyType.CLIENT_ATTRIBUTE, "temperature", deviceTemperatures);
deviceService.deleteDevicesByTenantId(tenantId);
}
@@ -1843,24 +1675,10 @@ public class EntityServiceTest extends AbstractServiceTest {
List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "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(67, loadedEntities.size());
- List loadedTemperatures = new ArrayList<>();
- for (Device device : devices) {
- loadedTemperatures.add(loadedEntities.stream().filter(entityData -> entityData.getEntityId().equals(device.getId())).findFirst().orElse(null)
- .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
- }
List deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
- Assert.assertEquals(deviceTemperatures, loadedTemperatures);
+ EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.TIME_SERIES, "temperature", deviceTemperatures);
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter();
@@ -1871,23 +1689,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.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList());
List deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
- Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
+ query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
+ findByQueryAndCheckTelemetry(query, EntityKeyType.TIME_SERIES, "temperature", deviceHighTemperatures);
deviceService.deleteDevicesByTenantId(tenantId);
}
@@ -1995,7 +1800,7 @@ public class EntityServiceTest extends AbstractServiceTest {
EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder);
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFiltersEqualString);
- PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
+ PageData