Browse Source

Merge branch 'master' into task/5416-mobile-action-device-provisioning

pull/12467/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
8378b2f94c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/pom.xml
  2. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 17
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  4. 7
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  5. 10
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  6. 21
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  7. 10
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java
  9. 11
      application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java
  10. 1
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  11. 5
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  12. 33
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  13. 19
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  16. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  18. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  19. 117
      application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java
  20. 298
      application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java
  21. 61
      application/src/main/java/org/thingsboard/server/service/edqs/EdqsListener.java
  22. 284
      application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java
  23. 42
      application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java
  24. 35
      application/src/main/java/org/thingsboard/server/service/edqs/LocalEdqsSyncService.java
  25. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  26. 45
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  27. 14
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  28. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  29. 8
      application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java
  30. 2
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  31. 180
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateManager.java
  32. 106
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  33. 196
      application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java
  34. 6
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  35. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  36. 12
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  37. 3
      application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java
  38. 1
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  39. 55
      application/src/main/resources/thingsboard.yml
  40. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  41. 72
      application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
  42. 198
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  43. 17
      application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
  44. 4
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  45. 6
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  46. 128
      application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java
  47. 799
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  48. 93
      application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java
  49. 11
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
  50. 61
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java
  51. 66
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  52. 28
      application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
  53. 3
      application/src/test/resources/application-test.properties
  54. 6
      application/src/test/resources/update/330/device_profile_001_out.json
  55. 2
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java
  56. 10
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueResponseTemplate.java
  57. 89
      common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java
  58. 1
      common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java
  59. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  60. 75
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java
  61. 40
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java
  62. 34
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEvent.java
  63. 21
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEventType.java
  64. 32
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java
  65. 24
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java
  66. 68
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java
  67. 70
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java
  68. 32
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java
  69. 38
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java
  70. 65
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java
  71. 59
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java
  72. 58
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetFields.java
  73. 35
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetProfileFields.java
  74. 55
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/CustomerFields.java
  75. 61
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.java
  76. 58
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceFields.java
  77. 38
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceProfileFields.java
  78. 43
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EdgeFields.java
  79. 179
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java
  80. 36
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityIdFields.java
  81. 37
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java
  82. 299
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java
  83. 29
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java
  84. 26
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ProfileAwareFields.java
  85. 42
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/QueueStatsFields.java
  86. 38
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleChainFields.java
  87. 43
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleNodeFields.java
  88. 63
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java
  89. 36
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantProfileFields.java
  90. 48
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java
  91. 30
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetTypeFields.java
  92. 30
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetsBundleFields.java
  93. 34
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java
  94. 35
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsResponse.java
  95. 39
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java
  96. 6
      common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java
  97. 17
      common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java
  98. 2
      common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java
  99. 2
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java
  100. 15
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java

4
application/pom.xml

@ -124,6 +124,10 @@
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<artifactId>edge-api</artifactId> <artifactId>edge-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>edqs</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>dao</artifactId> <artifactId>dao</artifactId>

10
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.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter; 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.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; 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.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; 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.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache; import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
@ -233,7 +234,7 @@ public class ActorSystemContext {
@Autowired(required = false) @Autowired(required = false)
@Getter @Getter
private RuleEngineDeviceStateManager deviceStateManager; private DeviceStateManager deviceStateManager;
@Autowired @Autowired
@Getter @Getter
@ -544,6 +545,11 @@ public class ActorSystemContext {
@Getter @Getter
private CalculatedFieldStateService calculatedFieldStateService; private CalculatedFieldStateService calculatedFieldStateService;
@Lazy
@Autowired(required = false)
@Getter
private CalculatedFieldQueueService calculatedFieldQueueService;
@Lazy @Lazy
@Autowired(required = false) @Autowired(required = false)
@Getter @Getter

17
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) { public void process(CalculatedFieldEntityDeleteMsg msg) {
log.info("[{}] Processing CF entity delete msg.", msg.getEntityId()); log.info("[{}] Processing CF entity delete msg.", msg.getEntityId());
if (this.entityId.equals(msg.getEntityId())) { if (this.entityId.equals(msg.getEntityId())) {
MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); if (states.isEmpty()) {
states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); msg.getCallback().onSuccess();
ctx.stop(ctx.getSelf()); } 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 { } else {
var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var state = states.remove(cfId); var state = states.remove(cfId);
if (state != null) { if (state != null) {
cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback());
} else {
msg.getCallback().onSuccess();
} }
} }
} }
@ -242,7 +248,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
justRestored = true; justRestored = true;
} }
if (state.isSizeOk()) { if (state.isSizeOk()) {
if (state.updateState(newArgValues) || justRestored) { if (state.updateState(ctx, newArgValues) || justRestored) {
cfIdList = new ArrayList<>(cfIdList); cfIdList = new ArrayList<>(cfIdList);
cfIdList.add(ctx.getCfId()); cfIdList.add(ctx.getCfId());
processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); 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. // We remove the state, but remember that it is over-sized in a local map.
cfStateService.removeState(ctxId, new TbCallback() { cfStateService.removeState(ctxId, new TbCallback() {
@Override @Override
@ -314,6 +320,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
callback.onFailure(ex); callback.onFailure(ex);
} }
}); });
throw ex;
} }
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, List<TsKvProto> data) { private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, List<TsKvProto> data) {

7
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -16,7 +16,6 @@
package org.thingsboard.server.actors.calculatedField; package org.thingsboard.server.actors.calculatedField;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
@ -51,8 +50,6 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
@ -202,7 +199,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (fieldsCount > 0) { if (fieldsCount > 0) {
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
var entityId = msg.getEntityId(); 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)); newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback));
} else { } else {
callback.onSuccess(); callback.onSuccess();
@ -309,7 +306,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (!entityIds.isEmpty()) { if (!entityIds.isEmpty()) {
//TODO: no need to do this if we cache all created actors and know which one belong to us; //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); var multiCallback = new MultipleTbCallback(entityIds.size(), callback);
entityIds.forEach(id -> deleteCfForEntity(entityId, cfId, multiCallback)); entityIds.forEach(id -> deleteCfForEntity(id, cfId, multiCallback));
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }

10
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.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; 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.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.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
@ -725,7 +726,7 @@ public class DefaultTbContext implements TbContext {
} }
@Override @Override
public RuleEngineDeviceStateManager getDeviceStateManager() { public DeviceStateManager getDeviceStateManager() {
return mainCtx.getDeviceStateManager(); return mainCtx.getDeviceStateManager();
} }
@ -902,6 +903,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getCalculatedFieldService(); return mainCtx.getCalculatedFieldService();
} }
@Override
public RuleEngineCalculatedFieldQueueService getCalculatedFieldQueueService() {
return mainCtx.getCalculatedFieldQueueService();
}
@Override @Override
public boolean isExternalNodeForceAck() { public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck(); return mainCtx.isExternalNodeForceAck();

21
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.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; 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.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; 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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; 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.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; 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.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.query.EntityQueryService;
@ -55,6 +60,10 @@ public class EntityQueryController extends BaseController {
@Autowired @Autowired
private EntityQueryService entityQueryService; private EntityQueryService entityQueryService;
@Autowired
private EdqsService edqsService;
@Autowired
private EdqsApiService edqsApiService;
private static final int MAX_PAGE_SIZE = 100; 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); 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();
}
} }

10
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.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -160,7 +161,12 @@ public class TenantProfileController extends BaseController {
" \"rpcTtlDays\": 0,\n" + " \"rpcTtlDays\": 0,\n" +
" \"queueStatsTtlDays\": 0,\n" + " \"queueStatsTtlDays\": 0,\n" +
" \"ruleEngineExceptionsTtlDays\": 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" +
" },\n" + " },\n" +
" \"default\": false\n" + " \"default\": false\n" +
@ -172,7 +178,7 @@ public class TenantProfileController extends BaseController {
@RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.") 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; TenantProfile oldProfile;
if (tenantProfile.getId() == null) { if (tenantProfile.getId() == null) {
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE);

3
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 com.google.common.util.concurrent.FutureCallback;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest; 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.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import java.util.List; 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; * Filter CFs based on the request entity. Push to the queue if any matching CF exist;

11
application/src/main/java/org/thingsboard/server/service/cf/CfRocksDb.java

@ -15,22 +15,29 @@
*/ */
package org.thingsboard.server.service.cf; package org.thingsboard.server.service.cf;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import org.rocksdb.Options; import org.rocksdb.Options;
import org.rocksdb.WriteOptions; import org.rocksdb.WriteOptions;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.utils.TbRocksDb; import org.thingsboard.server.edqs.util.TbRocksDb;
@Component @Component
@ConditionalOnExpression("'${queue.type:null}'=='in-memory'") @ConditionalOnExpression("'${queue.type:null}'=='in-memory'")
public class CfRocksDb extends TbRocksDb { 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)); super(path, new Options().setCreateIfMissing(true), new WriteOptions().setSync(true));
} }
@PostConstruct
@Override
public void init() {
super.init();
}
@PreDestroy @PreDestroy
@Override @Override
public void close() { public void close() {

1
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -117,7 +117,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
CalculatedField calculatedField = getCalculatedField(calculatedFieldId); CalculatedField calculatedField = getCalculatedField(calculatedFieldId);
if (calculatedField != null) { if (calculatedField != null) {
ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService);
ctx.init();
calculatedFieldsCtx.put(calculatedFieldId, ctx); calculatedFieldsCtx.put(calculatedFieldId, ctx);
log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx);
} }

5
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(() -> { return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx); var result = createStateByType(ctx);
result.updateState(argFutures.entrySet().stream() result.updateState(ctx, argFutures.entrySet().stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
Entry::getKey, // Keep the key as is Entry::getKey, // Keep the key as is
entry -> { entry -> {
@ -273,7 +273,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
long startTs = currentTime - timeWindow; long startTs = currentTime - timeWindow;
long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); 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); ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE);
ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));

33
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.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -89,6 +90,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback); () -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
} }
@Override
public void pushRequestToQueue(TimeseriesSaveRequest request, FutureCallback<Void> callback) {
pushRequestToQueue(request, null, callback);
}
@Override @Override
public void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback) { public void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback) {
var tenantId = request.getTenantId(); var tenantId = request.getTenantId();
@ -97,6 +103,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
() -> toCalculatedFieldTelemetryMsgProto(request, result), callback); () -> toCalculatedFieldTelemetryMsgProto(request, result), callback);
} }
@Override
public void pushRequestToQueue(AttributesSaveRequest request, FutureCallback<Void> callback) {
pushRequestToQueue(request, null, callback);
}
@Override @Override
public void pushRequestToQueue(AttributesDeleteRequest request, List<String> result, FutureCallback<Void> callback) { public void pushRequestToQueue(AttributesDeleteRequest request, List<String> result, FutureCallback<Void> callback) {
var tenantId = request.getTenantId(); var tenantId = request.getTenantId();
@ -156,15 +167,19 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder();
CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType()); CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType());
List<TsKvEntry> entries = request.getEntries(); List<TsKvEntry> entries = request.getEntries();
List<Long> versions = result.getVersions(); List<Long> versions = result != null ? result.getVersions() : Collections.emptyList();
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
long tsVersion = versions.get(i); TsKvProto.Builder tsProtoBuilder = toTsKvProto(entries.get(i)).toBuilder();
TsKvProto tsProto = toTsKvProto(entries.get(i)).toBuilder().setVersion(tsVersion).build(); if (result != null) {
telemetryMsg.addTsData(tsProto); tsProtoBuilder.setVersion(versions.get(i));
}
telemetryMsg.addTsData(tsProtoBuilder.build());
} }
msg.setTelemetryMsg(telemetryMsg.build());
msg.setTelemetryMsg(telemetryMsg.build());
return msg.build(); return msg.build();
} }
@ -175,9 +190,11 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name())); telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name()));
List<AttributeKvEntry> entries = request.getEntries(); List<AttributeKvEntry> entries = request.getEntries();
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
long attrVersion = versions.get(i); AttributeValueProto.Builder attrProtoBuilder = ProtoUtils.toProto(entries.get(i)).toBuilder();
AttributeValueProto attrProto = ProtoUtils.toProto(entries.get(i)).toBuilder().setVersion(attrVersion).build(); if (versions != null) {
telemetryMsg.addAttrData(attrProto); attrProtoBuilder.setVersion(versions.get(i));
}
telemetryMsg.addAttrData(attrProtoBuilder.build());
} }
msg.setTelemetryMsg(telemetryMsg.build()); msg.setTelemetryMsg(telemetryMsg.build());

19
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.List;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public abstract class BaseCalculatedFieldState implements CalculatedFieldState { public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
@ -43,7 +45,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
} }
@Override @Override
public boolean updateState(Map<String, ArgumentEntry> argumentValues) { public boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
if (arguments == null) { if (arguments == null) {
arguments = new HashMap<>(); arguments = new HashMap<>();
} }
@ -53,6 +55,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
for (Map.Entry<String, ArgumentEntry> entry : argumentValues.entrySet()) { for (Map.Entry<String, ArgumentEntry> entry : argumentValues.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
ArgumentEntry newEntry = entry.getValue(); ArgumentEntry newEntry = entry.getValue();
checkArgumentSize(key, newEntry, ctx);
ArgumentEntry existingEntry = arguments.get(key); ArgumentEntry existingEntry = arguments.get(key);
if (existingEntry == null || newEntry.isForceResetPrevious()) { 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); protected abstract void validateNewEntry(ArgumentEntry newEntry);
} }

2
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 maxDataPointsPerRollingArg;
private long maxStateSize; private long maxStateSize;
private long maxSingleValueArgumentSize;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) {
this.calculatedField = calculatedField; this.calculatedField = calculatedField;
@ -104,6 +105,7 @@ public class CalculatedFieldCtx {
this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024;
this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
} }
public void init() { public void init() {

4
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -44,7 +44,7 @@ public interface CalculatedFieldState {
void setRequiredArguments(List<String> requiredArguments); void setRequiredArguments(List<String> requiredArguments);
boolean updateState(Map<String, ArgumentEntry> argumentValues); boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx); ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx);
@ -60,4 +60,6 @@ public interface CalculatedFieldState {
void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize);
void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx);
} }

8
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) { public SingleValueArgumentEntry(TsKvProto entry) {
this.ts = entry.getTs(); this.ts = entry.getTs();
this.version = entry.getVersion(); if (entry.hasVersion()) {
this.version = entry.getVersion();
}
this.kvEntryValue = ProtoUtils.fromProto(entry.getKv()); this.kvEntryValue = ProtoUtils.fromProto(entry.getKv());
} }
public SingleValueArgumentEntry(AttributeValueProto entry) { public SingleValueArgumentEntry(AttributeValueProto entry) {
this.ts = entry.getLastUpdateTs(); this.ts = entry.getLastUpdateTs();
this.version = entry.getVersion(); if (entry.hasVersion()) {
this.version = entry.getVersion();
}
this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry); this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry);
} }

5
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -82,6 +82,11 @@ public class EdgeEventSourcingListener {
@TransactionalEventListener(fallbackExecution = true) @TransactionalEventListener(fallbackExecution = true)
public void handleEvent(SaveEntityEvent<?> event) { public void handleEvent(SaveEntityEvent<?> event) {
if (Boolean.FALSE.equals(event.getBroadcastEvent())) {
log.trace("Ignoring event {}", event);
return;
}
try { try {
if (!isValidSaveEntityEventForEdgeProcessing(event)) { if (!isValidSaveEntityEventForEdgeProcessing(event)) {
return; return;

5
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<>(); entityData = new HashMap<>();
attributes = JacksonUtil.newObjectNode(); attributes = JacksonUtil.newObjectNode();
for (AttributeKvEntry attr : ssAttributes) { for (AttributeKvEntry attr : ssAttributes) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) if (DefaultDeviceStateService.ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT.contains(attr.getKey())) {
&& !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
continue; continue;
} }
if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) {
@ -200,7 +199,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
} }
Map<Long, Map<String, Object>> tsData = new HashMap<>(); Map<Long, Map<String, Object>> tsData = new HashMap<>();
for (TsKvEntry tsKvEntry : tsKvEntries) { for (TsKvEntry tsKvEntry : tsKvEntries) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(tsKvEntry.getKey())) { if (DefaultDeviceStateService.ACTIVITY_KEYS_WITH_INACTIVITY_TIMEOUT.contains(tsKvEntry.getKey())) {
continue; continue;
} }
tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue()); tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue());

117
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<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>> 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<EdqsResponse> 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<TbProtoQueueMsg<FromEdqsMsg>> 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();
}
}

298
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
}
}

61
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());
}
}
}

284
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<UUID, EntityIdInfo> entityInfoMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, String> keys = new ConcurrentHashMap<>();
private final Map<ObjectType, AtomicInteger> 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<RelationEntity> 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<RelationEntity> 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<AttributeKvEntity> 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<AttributeKvEntity> 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<TsKvLatestEntity> 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<TsKvLatestEntity> 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) {
}
}

42
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;
}
}

35
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();
}
}

5
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -77,6 +77,11 @@ public class EntityStateSourcingListener {
@TransactionalEventListener(fallbackExecution = true) @TransactionalEventListener(fallbackExecution = true)
public void handleEvent(SaveEntityEvent<?> event) { public void handleEvent(SaveEntityEvent<?> event) {
if (Boolean.FALSE.equals(event.getBroadcastEvent())) {
log.trace("Ignoring event {}", event);
return;
}
TenantId tenantId = event.getTenantId(); TenantId tenantId = event.getTenantId();
EntityId entityId = event.getEntityId(); EntityId entityId = event.getEntityId();
if (entityId == null) { if (entityId == null) {

45
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 com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; 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.actors.ActorSystemContext;
import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.common.data.JavaSerDesUtil;
import org.thingsboard.server.common.data.alarm.AlarmInfo; 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.ErrorEvent;
import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.event.LifecycleEvent; 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.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg; 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.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; 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.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; 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.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey; 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.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.resource.TbImageService;
@ -146,9 +147,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
private final TbCoreQueueFactory queueFactory; private final TbCoreQueueFactory queueFactory;
private final TbImageService imageService; private final TbImageService imageService;
private final RuleEngineCallService ruleEngineCallService; private final RuleEngineCallService ruleEngineCallService;
private final EdqsService edqsService;
private final TbCoreConsumerStats stats; private final TbCoreConsumerStats stats;
private MainQueueConsumerManager<TbProtoQueueMsg<ToCoreMsg>, CoreQueueConfig> mainConsumer; private MainQueueConsumerManager<TbProtoQueueMsg<ToCoreMsg>, QueueConfig> mainConsumer;
private QueueConsumerManager<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer; private QueueConsumerManager<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer;
private QueueConsumerManager<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> firmwareStatesConsumer; private QueueConsumerManager<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> firmwareStatesConsumer;
@ -175,7 +177,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationRuleProcessor notificationRuleProcessor, NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService, TbImageService imageService,
RuleEngineCallService ruleEngineCallService, RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache) { CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService); eventPublisher, jwtSettingsService);
this.stateService = stateService; this.stateService = stateService;
@ -191,6 +194,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
this.imageService = imageService; this.imageService = imageService;
this.ruleEngineCallService = ruleEngineCallService; this.ruleEngineCallService = ruleEngineCallService;
this.queueFactory = tbCoreQueueFactory; this.queueFactory = tbCoreQueueFactory;
this.edqsService = edqsService;
} }
@PostConstruct @PostConstruct
@ -198,9 +202,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
super.init("tb-core"); super.init("tb-core");
this.deviceActivityEventsExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-device-activity-events-executor"))); this.deviceActivityEventsExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-device-activity-events-executor")));
this.mainConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToCoreMsg>, CoreQueueConfig>builder() this.mainConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToCoreMsg>, QueueConfig>builder()
.queueKey(new QueueKey(ServiceType.TB_CORE)) .queueKey(new QueueKey(ServiceType.TB_CORE))
.config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval)) .config(QueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs) .msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createToCoreMsgConsumer()) .consumerCreator((config, partitionId) -> queueFactory.createToCoreMsgConsumer())
.consumerExecutor(consumersExecutor) .consumerExecutor(consumersExecutor)
@ -251,7 +255,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, CoreQueueConfig config) throws Exception { private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, QueueConfig config) throws Exception {
List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
@ -289,6 +293,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} else if (toCoreMsg.hasDeviceInactivityMsg()) { } else if (toCoreMsg.hasDeviceInactivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg()); log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg());
forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback); forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback);
} else if (toCoreMsg.hasDeviceInactivityTimeoutUpdateMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityTimeoutUpdateMsg());
forwardToStateService(toCoreMsg.getDeviceInactivityTimeoutUpdateMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorNotification()) { } else if (toCoreMsg.hasToDeviceActorNotification()) {
TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification()); TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification());
if (actorMsg != null) { if (actorMsg != null) {
@ -389,6 +396,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
callback.onSuccess(); callback.onSuccess();
} else if (toCoreNotification.hasResourceCacheInvalidateMsg()) { } else if (toCoreNotification.hasResourceCacheInvalidateMsg()) {
forwardToResourceService(toCoreNotification.getResourceCacheInvalidateMsg(), callback); forwardToResourceService(toCoreNotification.getResourceCacheInvalidateMsg(), callback);
} else if (toCoreNotification.hasToEdqsCoreServiceMsg()) {
edqsService.processSystemMsg(JacksonUtil.fromBytes(toCoreNotification.getToEdqsCoreServiceMsg().getValue().toByteArray(), ToCoreEdqsMsg.class));
callback.onSuccess();
} }
if (statsEnabled) { if (statsEnabled) {
stats.log(toCoreNotification); stats.log(toCoreNotification);
@ -651,6 +661,21 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}); });
} }
void forwardToStateService(TransportProtos.DeviceInactivityTimeoutUpdateProto deviceInactivityTimeoutUpdateMsg, TbCallback callback) {
if (statsEnabled) {
stats.log(deviceInactivityTimeoutUpdateMsg);
}
var tenantId = toTenantId(deviceInactivityTimeoutUpdateMsg.getTenantIdMSB(), deviceInactivityTimeoutUpdateMsg.getTenantIdLSB());
var deviceId = new DeviceId(new UUID(deviceInactivityTimeoutUpdateMsg.getDeviceIdMSB(), deviceInactivityTimeoutUpdateMsg.getDeviceIdLSB()));
ListenableFuture<?> 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) { private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {
TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB()); TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB());
NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB())); NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()));
@ -723,10 +748,4 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
firmwareStatesConsumer.stop(); firmwareStatesConsumer.stop();
} }
@Data(staticConstructor = "of")
public static class CoreQueueConfig implements QueueConfig {
private final boolean consumerPerPartition;
private final int pollInterval;
}
} }

14
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -85,7 +85,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
private final EdgeContextComponent edgeCtx; private final EdgeContextComponent edgeCtx;
private final EdgeConsumerStats stats; private final EdgeConsumerStats stats;
private MainQueueConsumerManager<TbProtoQueueMsg<ToEdgeMsg>, EdgeQueueConfig> mainConsumer; private MainQueueConsumerManager<TbProtoQueueMsg<ToEdgeMsg>, QueueConfig> mainConsumer;
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) { StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
@ -100,9 +100,9 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public void init() { public void init() {
super.init("tb-edge"); super.init("tb-edge");
this.mainConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToEdgeMsg>, EdgeQueueConfig>builder() this.mainConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToEdgeMsg>, QueueConfig>builder()
.queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME)) .queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME))
.config(EdgeQueueConfig.of(consumerPerPartition, pollInterval)) .config(QueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs) .msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer()) .consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer())
.consumerExecutor(consumersExecutor) .consumerExecutor(consumersExecutor)
@ -128,7 +128,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
mainConsumer.update(partitions); mainConsumer.update(partitions);
} }
private void processMsgs(List<TbProtoQueueMsg<ToEdgeMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeMsg>> consumer, EdgeQueueConfig edgeQueueConfig) throws InterruptedException { private void processMsgs(List<TbProtoQueueMsg<ToEdgeMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeMsg>> consumer, QueueConfig edgeQueueConfig) throws InterruptedException {
List<IdMsgPair<ToEdgeMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<ToEdgeMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToEdgeMsg>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<ToEdgeMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
@ -285,10 +285,4 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
mainConsumer.awaitStop(); mainConsumer.awaitStop();
} }
@Data(staticConstructor = "of")
public static class EdgeQueueConfig implements QueueConfig {
private final boolean consumerPerPartition;
private final int pollInterval;
}
} }

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -107,7 +107,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent event) { protected void onTbApplicationEvent(PartitionChangeEvent event) {
event.getPartitionsMap().forEach((queueKey, partitions) -> { event.getNewPartitions().forEach((queueKey, partitions) -> {
if (CollectionsUtil.isOneOf(queueKey, QueueKey.CF, QueueKey.CF_STATES)) { if (CollectionsUtil.isOneOf(queueKey, QueueKey.CF, QueueKey.CF_STATES)) {
return; return;
} }

8
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_ACTIVITIES = "deviceActivity";
public static final String DEVICE_DISCONNECTS = "deviceDisconnect"; public static final String DEVICE_DISCONNECTS = "deviceDisconnect";
public static final String DEVICE_INACTIVITIES = "deviceInactivity"; 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_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine
public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl"; public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl";
@ -65,6 +66,7 @@ public class TbCoreConsumerStats {
private final StatsCounter deviceActivitiesCounter; private final StatsCounter deviceActivitiesCounter;
private final StatsCounter deviceDisconnectsCounter; private final StatsCounter deviceDisconnectsCounter;
private final StatsCounter deviceInactivitiesCounter; private final StatsCounter deviceInactivitiesCounter;
private final StatsCounter deviceInactivityTimeoutUpdatesCounter;
private final StatsCounter toCoreNfOtherCounter; private final StatsCounter toCoreNfOtherCounter;
private final StatsCounter toCoreNfComponentLifecycleCounter; private final StatsCounter toCoreNfComponentLifecycleCounter;
@ -95,6 +97,7 @@ public class TbCoreConsumerStats {
this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES)); this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES));
this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS)); this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS));
this.deviceInactivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITIES)); this.deviceInactivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITIES));
this.deviceInactivityTimeoutUpdatesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITY_TIMEOUT_UPDATES));
// Core notification counters // Core notification counters
this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER)); this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER));
@ -163,6 +166,11 @@ public class TbCoreConsumerStats {
deviceInactivitiesCounter.increment(); deviceInactivitiesCounter.increment();
} }
public void log(TransportProtos.DeviceInactivityTimeoutUpdateProto msg) {
totalCounter.increment();
deviceInactivityTimeoutUpdatesCounter.increment();
}
public void log(TransportProtos.SubscriptionMgrMsgProto msg) { public void log(TransportProtos.SubscriptionMgrMsgProto msg) {
totalCounter.increment(); totalCounter.increment();
subscriptionMsgCounter.increment(); subscriptionMsgCounter.increment();

2
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java

@ -72,7 +72,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
ExecutorService consumerExecutor, ExecutorService consumerExecutor,
ScheduledExecutorService scheduler, ScheduledExecutorService scheduler,
ExecutorService taskExecutor) { ExecutorService taskExecutor) {
super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor); super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor, null);
this.ctx = ctx; this.ctx = ctx;
this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory()); this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory());
} }

180
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateManager.java

@ -0,0 +1,180 @@
/**
* 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.state;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.SimpleTbQueueCallback;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
@Slf4j
@Service
@RequiredArgsConstructor
public class DefaultDeviceStateManager implements DeviceStateManager {
private final TbServiceInfoProvider serviceInfoProvider;
private final PartitionService partitionService;
private final Optional<DeviceStateService> 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<DeviceStateService> toDeviceStateService,
Supplier<TransportProtos.ToCoreMsg> 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));
}
}
}

106
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.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -129,11 +130,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private static final List<EntityKey> PERSISTENT_TELEMETRY_KEYS = Arrays.asList( private static final List<EntityKey> PERSISTENT_TELEMETRY_KEYS = Arrays.asList(
new EntityKey(EntityKeyType.TIME_SERIES, LAST_ACTIVITY_TIME), new EntityKey(EntityKeyType.TIME_SERIES, LAST_ACTIVITY_TIME),
new EntityKey(EntityKeyType.TIME_SERIES, INACTIVITY_ALARM_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, ACTIVITY_STATE),
new EntityKey(EntityKeyType.TIME_SERIES, LAST_CONNECT_TIME), new EntityKey(EntityKeyType.TIME_SERIES, LAST_CONNECT_TIME),
new EntityKey(EntityKeyType.TIME_SERIES, LAST_DISCONNECT_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<EntityKey> PERSISTENT_ATTRIBUTE_KEYS = Arrays.asList( private static final List<EntityKey> PERSISTENT_ATTRIBUTE_KEYS = Arrays.asList(
new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME), new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_ACTIVITY_TIME),
@ -143,8 +143,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_CONNECT_TIME), new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_CONNECT_TIME),
new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_DISCONNECT_TIME)); new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, LAST_DISCONNECT_TIME));
public static final List<String> PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, public static final Set<String> ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT = Set.of(
LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT); ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME
);
public static final Set<String> 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<EntityKey> PERSISTENT_ENTITY_FIELDS = Arrays.asList( private static final List<EntityKey> PERSISTENT_ENTITY_FIELDS = Arrays.asList(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "name"),
new EntityKey(EntityKeyType.ENTITY_FIELD, "type"), new EntityKey(EntityKeyType.ENTITY_FIELD, "type"),
@ -251,7 +257,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
log.trace("[{}][{}] On device connect: processing connect event with ts [{}].", tenantId.getId(), deviceId.getId(), lastConnectTime); log.trace("[{}][{}] On device connect: processing connect event with ts [{}].", tenantId.getId(), deviceId.getId(), lastConnectTime);
stateData.getState().setLastConnectTime(lastConnectTime); stateData.getState().setLastConnectTime(lastConnectTime);
save(deviceId, LAST_CONNECT_TIME, lastConnectTime); save(tenantId, deviceId, LAST_CONNECT_TIME, lastConnectTime);
pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT); pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT);
checkAndUpdateState(deviceId, stateData); checkAndUpdateState(deviceId, stateData);
} }
@ -271,14 +277,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) { void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) {
log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity); log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity);
if (stateData != null) { if (stateData != null) {
save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity); save(stateData.getTenantId(), deviceId, LAST_ACTIVITY_TIME, lastReportedActivity);
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
state.setLastActivityTime(lastReportedActivity); state.setLastActivityTime(lastReportedActivity);
if (!state.isActive()) { if (!state.isActive()) {
state.setActive(true); state.setActive(true);
if (lastReportedActivity <= state.getLastInactivityAlarmTime()) { if (lastReportedActivity <= state.getLastInactivityAlarmTime()) {
state.setLastInactivityAlarmTime(0); state.setLastInactivityAlarmTime(0);
save(deviceId, INACTIVITY_ALARM_TIME, 0); save(stateData.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, 0);
} }
onDeviceActivityStatusChange(deviceId, true, stateData); onDeviceActivityStatusChange(deviceId, true, stateData);
} }
@ -307,7 +313,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
log.trace("[{}][{}] On device disconnect: processing disconnect event with ts [{}].", tenantId.getId(), deviceId.getId(), lastDisconnectTime); log.trace("[{}][{}] On device disconnect: processing disconnect event with ts [{}].", tenantId.getId(), deviceId.getId(), lastDisconnectTime);
stateData.getState().setLastDisconnectTime(lastDisconnectTime); stateData.getState().setLastDisconnectTime(lastDisconnectTime);
save(deviceId, LAST_DISCONNECT_TIME, lastDisconnectTime); save(tenantId, deviceId, LAST_DISCONNECT_TIME, lastDisconnectTime);
pushRuleEngineMessage(stateData, TbMsgType.DISCONNECT_EVENT); pushRuleEngineMessage(stateData, TbMsgType.DISCONNECT_EVENT);
} }
@ -418,7 +424,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) { private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) {
DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState); DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState);
boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive(); boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive();
save(deviceId, ACTIVITY_STATE, activityState); save(fetchedState.getTenantId(), deviceId, ACTIVITY_STATE, activityState);
} }
@Override @Override
@ -496,7 +502,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
updateActivityState(deviceId, state, deviceState.getLastActivityTime()); updateActivityState(deviceId, state, deviceState.getLastActivityTime());
if (deviceState.getLastInactivityAlarmTime() != 0L && deviceState.getLastInactivityAlarmTime() >= deviceState.getLastActivityTime()) { if (deviceState.getLastInactivityAlarmTime() != 0L && deviceState.getLastInactivityAlarmTime() >= deviceState.getLastActivityTime()) {
deviceState.setLastInactivityAlarmTime(0L); 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<Dev
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
state.setActive(false); state.setActive(false);
state.setLastInactivityAlarmTime(ts); state.setLastInactivityAlarmTime(ts);
save(deviceId, INACTIVITY_ALARM_TIME, ts); save(stateData.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, ts);
onDeviceActivityStatusChange(deviceId, false, stateData); onDeviceActivityStatusChange(deviceId, false, stateData);
} }
@ -611,7 +617,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
private void onDeviceActivityStatusChange(DeviceId deviceId, boolean active, DeviceStateData stateData) { private void onDeviceActivityStatusChange(DeviceId deviceId, boolean active, DeviceStateData stateData) {
save(deviceId, ACTIVITY_STATE, active); save(stateData.getTenantId(), deviceId, ACTIVITY_STATE, active);
pushRuleEngineMessage(stateData, active ? TbMsgType.ACTIVITY_EVENT : TbMsgType.INACTIVITY_EVENT); pushRuleEngineMessage(stateData, active ? TbMsgType.ACTIVITY_EVENT : TbMsgType.INACTIVITY_EVENT);
TbMsgMetaData metaData = stateData.getMetaData(); TbMsgMetaData metaData = stateData.getMetaData();
notificationRuleProcessor.process(DeviceActivityTrigger.builder() notificationRuleProcessor.process(DeviceActivityTrigger.builder()
@ -643,41 +649,43 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
deviceStates.remove(deviceId); deviceStates.remove(deviceId);
} }
private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) { private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
ListenableFuture<DeviceStateData> future; ListenableFuture<DeviceStateData> future;
if (persistToTelemetry) { if (persistToTelemetry) {
ListenableFuture<List<TsKvEntry>> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES); ListenableFuture<List<TsKvEntry>> timeseriesActivityDataFuture = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), ACTIVITY_KEYS_WITHOUT_INACTIVITY_TIMEOUT);
future = Futures.transform(tsData, extractDeviceStateData(device), MoreExecutors.directExecutor()); ListenableFuture<Optional<AttributeKvEntry>> inactivityTimeoutAttributeFuture = attributesService.find(
TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, INACTIVITY_TIMEOUT
);
ListenableFuture<List<? extends KvEntry>> fullActivityDataFuture = Futures.whenAllSucceed(timeseriesActivityDataFuture, inactivityTimeoutAttributeFuture).call(() -> {
List<TsKvEntry> activityTimeseries = Futures.getDone(timeseriesActivityDataFuture);
Optional<AttributeKvEntry> inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture);
if (inactivityTimeoutAttribute.isPresent()) {
List<KvEntry> 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 { } else {
ListenableFuture<List<AttributeKvEntry>> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), AttributeScope.SERVER_SCOPE, PERSISTENT_ATTRIBUTES); ListenableFuture<List<AttributeKvEntry>> attributesActivityDataFuture = attributesService.find(
future = Futures.transform(attrData, extractDeviceStateData(device), MoreExecutors.directExecutor()); 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<DeviceStateData> transformInactivityTimeout(ListenableFuture<DeviceStateData> future) { private Function<List<? extends KvEntry>, DeviceStateData> extractDeviceStateData(Device device) {
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 <T extends KvEntry> Function<List<T>, DeviceStateData> extractDeviceStateData(Device device) {
return new Function<>() { return new Function<>() {
@Nonnull @Nonnull
@Override @Override
public DeviceStateData apply(@Nullable List<T> data) { public DeviceStateData apply(@Nullable List<? extends KvEntry> data) {
try { try {
long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L); long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L); long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L);
@ -690,7 +698,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.lastDisconnectTime(getEntryValue(data, LAST_DISCONNECT_TIME, 0L)) .lastDisconnectTime(getEntryValue(data, LAST_DISCONNECT_TIME, 0L))
.lastActivityTime(lastActivityTime) .lastActivityTime(lastActivityTime)
.lastInactivityAlarmTime(inactivityAlarmTime) .lastInactivityAlarmTime(inactivityAlarmTime)
.inactivityTimeout(inactivityTimeout) .inactivityTimeout(inactivityTimeout > 0 ? inactivityTimeout : defaultInactivityTimeoutMs)
.build(); .build();
TbMsgMetaData md = new TbMsgMetaData(); TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName()); md.putValue("deviceName", device.getName());
@ -761,12 +769,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
DeviceStateData toDeviceStateData(EntityData ed, DeviceIdInfo deviceIdInfo) { DeviceStateData toDeviceStateData(EntityData ed, DeviceIdInfo deviceIdInfo) {
long lastActivityTime = getEntryValue(ed, getKeyType(), LAST_ACTIVITY_TIME, 0L); long lastActivityTime = getEntryValue(ed, getKeyType(), LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(ed, getKeyType(), INACTIVITY_ALARM_TIME, 0L); long inactivityAlarmTime = getEntryValue(ed, getKeyType(), INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getEntryValue(ed, getKeyType(), INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs); long inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
if (persistToTelemetry && inactivityTimeout == defaultInactivityTimeoutMs) {
log.trace("[{}] default value for inactivity timeout fetched {}, going to fetch inactivity timeout from attributes",
deviceIdInfo.getDeviceId(), inactivityTimeout);
inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
}
// Actual active state by wall-clock will be updated outside this method. This method is only for fetching persistent state // Actual active state by wall-clock will be updated outside this method. This method is only for fetching persistent state
final boolean active = getEntryValue(ed, getKeyType(), ACTIVITY_STATE, false); final boolean active = getEntryValue(ed, getKeyType(), ACTIVITY_STATE, false);
DeviceState deviceState = DeviceState.builder() DeviceState deviceState = DeviceState.builder()
@ -874,18 +877,18 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
} }
private void save(DeviceId deviceId, String key, long value) { private void save(TenantId tenantId, DeviceId deviceId, String key, long value) {
save(deviceId, new LongDataEntry(key, value), getCurrentTimeMillis()); save(tenantId, deviceId, new LongDataEntry(key, value), getCurrentTimeMillis());
} }
private void save(DeviceId deviceId, String key, boolean value) { private void save(TenantId tenantId, DeviceId deviceId, String key, boolean value) {
save(deviceId, new BooleanDataEntry(key, value), getCurrentTimeMillis()); save(tenantId, deviceId, new BooleanDataEntry(key, value), getCurrentTimeMillis());
} }
private void save(DeviceId deviceId, KvEntry kvEntry, long ts) { private void save(TenantId tenantId, DeviceId deviceId, KvEntry kvEntry, long ts) {
if (persistToTelemetry) { if (persistToTelemetry) {
tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder()
.tenantId(TenantId.SYS_TENANT_ID) .tenantId(tenantId)
.entityId(deviceId) .entityId(deviceId)
.entry(new BasicTsKvEntry(ts, kvEntry)) .entry(new BasicTsKvEntry(ts, kvEntry))
.ttl(telemetryTtl) .ttl(telemetryTtl)
@ -893,7 +896,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.build()); .build());
} else { } else {
tsSubService.saveAttributes(AttributesSaveRequest.builder() tsSubService.saveAttributes(AttributesSaveRequest.builder()
.tenantId(TenantId.SYS_TENANT_ID) .tenantId(tenantId)
.entityId(deviceId) .entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE) .scope(AttributeScope.SERVER_SCOPE)
.entry(new BaseAttributeKvEntry(ts, kvEntry)) .entry(new BaseAttributeKvEntry(ts, kvEntry))
@ -925,4 +928,5 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
log.warn("[{}] Failed to update entry {}", deviceId, kvEntry, t); log.warn("[{}] Failed to update entry {}", deviceId, kvEntry, t);
} }
} }
} }

196
application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java

@ -1,196 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.state;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.SimpleTbQueueCallback;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Service
@TbRuleEngineComponent
public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager {
private final TbServiceInfoProvider serviceInfoProvider;
private final PartitionService partitionService;
private final Optional<DeviceStateService> deviceStateService;
private final TbClusterService clusterService;
public DefaultRuleEngineDeviceStateManager(
TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService,
Optional<DeviceStateService> 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));
}
}
}

6
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -161,9 +161,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
@Override @Override
public void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, TbCallback callback) { public void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, TbCallback callback) {
onTimeSeriesUpdate(entityId, ts); onTimeSeriesUpdate(entityId, ts);
if (entityId.getEntityType() == EntityType.DEVICE) {
updateDeviceInactivityTimeout(tenantId, entityId, ts);
}
callback.onSuccess(); callback.onSuccess();
} }
@ -171,9 +168,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
public void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback) { public void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback) {
onTimeSeriesUpdate(entityId, onTimeSeriesUpdate(entityId,
keys.stream().map(key -> new BasicTsKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); keys.stream().map(key -> new BasicTsKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList()));
if (entityId.getEntityType() == EntityType.DEVICE) {
deleteDeviceInactivityTimeout(tenantId, entityId, keys);
}
callback.onSuccess(); callback.onSuccess();
} }

3
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.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants; 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.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
@ -359,7 +360,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder();
EntityDataSortOrder entitiesSortOrder; EntityDataSortOrder entitiesSortOrder;
if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { 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 { } else {
entitiesSortOrder = sortOrder; entitiesSortOrder = sortOrder;
} }

12
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) .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
.callback(new FutureCallback<>() { .callback(new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Void tmp) { public void onSuccess(@Nullable Void tmp) {}
}
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
@ -324,6 +323,10 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
addMainCallback(saveFuture, result -> callback.onSuccess(null), callback::onFailure); addMainCallback(saveFuture, result -> callback.onSuccess(null), callback::onFailure);
} }
private <S> void addMainCallback(ListenableFuture<S> saveFuture, Consumer<S> onSuccess) {
addMainCallback(saveFuture, onSuccess, null);
}
private <S> void addMainCallback(ListenableFuture<S> saveFuture, Consumer<S> onSuccess, Consumer<Throwable> onFailure) { private <S> void addMainCallback(ListenableFuture<S> saveFuture, Consumer<S> onSuccess, Consumer<Throwable> onFailure) {
DonAsynchron.withCallback(saveFuture, onSuccess, onFailure, tsCallBackExecutor); DonAsynchron.withCallback(saveFuture, onSuccess, onFailure, tsCallBackExecutor);
} }
@ -345,13 +348,12 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {}
}
}; };
} }
private FutureCallback<Void> getCalculatedFieldCallback(FutureCallback<List<String>> originalCallback, List<String> keys) { private FutureCallback<Void> getCalculatedFieldCallback(FutureCallback<List<String>> originalCallback, List<String> keys) {
return new FutureCallback<Void>() { return new FutureCallback<>() {
@Override @Override
public void onSuccess(Void unused) { public void onSuccess(Void unused) {
originalCallback.onSuccess(keys); originalCallback.onSuccess(keys);

3
application/src/main/java/org/thingsboard/server/service/transport/TbCoreTransportApiService.java

@ -93,7 +93,8 @@ public class TbCoreTransportApiService {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
log.info("Received application ready event. Starting polling for events."); log.info("Received application ready event. Starting polling for events.");
transportApiTemplate.init(transportApiService); transportApiTemplate.subscribe();
transportApiTemplate.launch(transportApiService);
} }
@PreDestroy @PreDestroy

1
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -229,6 +229,7 @@ public class DefaultWebSocketService implements WebSocketService {
} catch (TbRateLimitsException e) { } catch (TbRateLimitsException e) {
log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e);
} catch (Exception e) { } catch (Exception e) {
sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, e.getMessage());
log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e);
} }
} }

55
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 "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 "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 "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: sink:
# Type of external sink. possible options: none, elasticsearch # Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}" type: "${AUDIT_LOG_SINK_TYPE:none}"
@ -1599,6 +1600,16 @@ queue:
- key: max.poll.records - 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 # 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}" 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-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 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 # - 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}" 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 # 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}" 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: consumer-stats:
# Prints lag between consumer group offset and last messages offset in Kafka topics # Prints lag between consumer group offset and last messages offset in Kafka topics
enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}"
@ -1707,6 +1724,44 @@ queue:
enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}"
# Statistics printing interval for Housekeeper # Statistics printing interval for Housekeeper
print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" 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: vc:
# Default topic name # Default topic name
topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}"

2
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -402,7 +402,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(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); .until(() -> storage.getLag("tb_housekeeper") == 0);
} }

72
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<EntityData> 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);
}
}

198
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.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions; 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.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; 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.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; 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.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.FilterPredicateValue;
@ -70,9 +73,11 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@ -130,36 +135,25 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
filter.setDeviceNameFilter(""); filter.setDeviceNameFilter("");
EntityCountQuery countQuery = new EntityCountQuery(filter); EntityCountQuery countQuery = new EntityCountQuery(filter);
countByQueryAndCheck(countQuery, 97);
Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
filter.setDeviceTypes(List.of("unknown")); filter.setDeviceTypes(List.of("unknown"));
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); countByQueryAndCheck(countQuery, 0);
Assert.assertEquals(0, count.longValue());
filter.setDeviceTypes(List.of("default")); filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter("Device1"); filter.setDeviceNameFilter("Device1");
countByQueryAndCheck(countQuery, 11);
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(11, count.longValue());
EntityListFilter entityListFilter = new EntityListFilter(); EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.DEVICE); entityListFilter.setEntityType(EntityType.DEVICE);
entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList()));
countQuery = new EntityCountQuery(entityListFilter); countQuery = new EntityCountQuery(entityListFilter);
countByQueryAndCheck(countQuery, 97);
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
EntityTypeFilter filter2 = new EntityTypeFilter(); EntityTypeFilter filter2 = new EntityTypeFilter();
filter2.setEntityType(EntityType.DEVICE); filter2.setEntityType(EntityType.DEVICE);
countQuery = new EntityCountQuery(filter2);
EntityCountQuery countQuery2 = new EntityCountQuery(filter2); countByQueryAndCheck(countQuery, 97);
Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class);
Assert.assertEquals(97, count2.longValue());
} }
@Test @Test
@ -169,51 +163,44 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityTypeFilter allDeviceFilter = new EntityTypeFilter(); EntityTypeFilter allDeviceFilter = new EntityTypeFilter();
allDeviceFilter.setEntityType(EntityType.DEVICE); allDeviceFilter.setEntityType(EntityType.DEVICE);
EntityCountQuery query = new EntityCountQuery(allDeviceFilter); EntityCountQuery query = new EntityCountQuery(allDeviceFilter);
Long initialCount = doPostWithResponse("/api/entitiesQuery/count", query, Long.class); countByQueryAndCheck(query, 0);
loginTenantAdmin(); loginTenantAdmin();
List<Device> devices = new ArrayList<>(); List<Device> devices = new ArrayList<>();
String devicePrefix = "Device" + RandomStringUtils.randomAlphabetic(5);
for (int i = 0; i < 97; i++) { for (int i = 0; i < 97; i++) {
Device device = new Device(); Device device = new Device();
device.setName("Device" + i); device.setName(devicePrefix + i);
device.setType("default"); device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000)); device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class)); devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1); Thread.sleep(1);
} }
DeviceTypeFilter filter = new DeviceTypeFilter(); DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default"); filter.setDeviceTypes(List.of("default"));
filter.setDeviceNameFilter(""); filter.setDeviceNameFilter("");
loginSysAdmin(); loginSysAdmin();
EntityCountQuery countQuery = new EntityCountQuery(filter); EntityCountQuery countQuery = new EntityCountQuery(filter);
countByQueryAndCheck(countQuery, 97);
Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); filter.setDeviceTypes(List.of("unknown"));
Assert.assertEquals(97, count.longValue()); countByQueryAndCheck(countQuery, 0);
filter.setDeviceType("unknown");
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(0, count.longValue());
filter.setDeviceType("default");
filter.setDeviceNameFilter("Device1");
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); filter.setDeviceTypes(List.of("default"));
Assert.assertEquals(11, count.longValue()); filter.setDeviceNameFilter(devicePrefix + "1");
countByQueryAndCheck(countQuery, 11);
EntityListFilter entityListFilter = new EntityListFilter(); EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityType(EntityType.DEVICE); entityListFilter.setEntityType(EntityType.DEVICE);
entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList()));
countQuery = new EntityCountQuery(entityListFilter); countQuery = new EntityCountQuery(entityListFilter);
countByQueryAndCheck(countQuery, 97);
count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); countByQueryAndCheck(countQuery, 97);
Assert.assertEquals(97, count.longValue());
Long count2 = doPostWithResponse("/api/entitiesQuery/count", query, Long.class);
Assert.assertEquals(initialCount + 97, count2.longValue());
} }
@Test @Test
@ -371,11 +358,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> data = PageData<EntityData> data = findByQueryAndCheck(query, 97);
doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(97, data.getTotalElements());
Assert.assertEquals(10, data.getTotalPages()); Assert.assertEquals(10, data.getTotalPages());
Assert.assertTrue(data.hasNext()); Assert.assertTrue(data.hasNext());
Assert.assertEquals(10, data.getData().size()); Assert.assertEquals(10, data.getData().size());
@ -383,8 +366,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List<EntityData> loadedEntities = new ArrayList<>(data.getData()); List<EntityData> loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) { while (data.hasNext()) {
query = query.next(); query = query.next();
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() { data = findByQuery(query);
});
loadedEntities.addAll(data.getData()); loadedEntities.addAll(data.getData());
} }
Assert.assertEquals(97, loadedEntities.size()); Assert.assertEquals(97, loadedEntities.size());
@ -406,8 +388,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder); pageLink = new EntityDataPageLink(10, 0, "device1", sortOrder);
query = new EntityDataQuery(filter, pageLink, entityFields, null, null); query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() { data = findByQuery(query);
});
Assert.assertEquals(11, data.getTotalElements()); Assert.assertEquals(11, data.getTotalElements());
Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); 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); EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null);
PageData<EntityData> data2 = PageData<EntityData> data2 = findByQuery(query2);
doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(97, data2.getTotalElements()); Assert.assertEquals(97, data2.getTotalElements());
Assert.assertEquals(10, data2.getTotalPages()); Assert.assertEquals(10, data2.getTotalPages());
@ -473,20 +452,15 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
findByQueryAndCheck(query, 87);
PageData<EntityData> data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(87, data.getTotalElements());
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false))); filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null); query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); findByQueryAndCheck(query, 10);
Assert.assertEquals(10, data.getTotalElements());
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true))); filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null); query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); findByQueryAndCheck(query, 87);
Assert.assertEquals(87, data.getTotalElements());
} }
private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) { private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) {
@ -531,14 +505,12 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List<EntityKey> latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); List<EntityKey> latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
PageData<EntityData> data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() { PageData<EntityData> data = findByQueryAndCheck(query, 67);
});
List<EntityData> loadedEntities = new ArrayList<>(data.getData()); List<EntityData> loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) { while (data.hasNext()) {
query = query.next(); query = query.next();
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() { data = findByQuery(query);
});
loadedEntities.addAll(data.getData()); loadedEntities.addAll(data.getData());
} }
Assert.assertEquals(67, loadedEntities.size()); Assert.assertEquals(67, loadedEntities.size());
@ -551,6 +523,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
pageLink = new EntityDataPageLink(10, 0, null, sortOrder); pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter(); KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate(); NumericFilterPredicate predicate = new NumericFilterPredicate();
predicate.setValue(FilterPredicateValue.fromDouble(45)); predicate.setValue(FilterPredicateValue.fromDouble(45));
predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
@ -559,13 +532,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() { data = findByQuery(query);
});
loadedEntities = new ArrayList<>(data.getData()); loadedEntities = new ArrayList<>(data.getData());
while (data.hasNext()) { while (data.hasNext()) {
query = query.next(); query = query.next();
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() { data = findByQuery(query);
});
loadedEntities.addAll(data.getData()); loadedEntities.addAll(data.getData());
} }
Assert.assertEquals(highTemperatures.size(), loadedEntities.size()); Assert.assertEquals(highTemperatures.size(), loadedEntities.size());
@ -604,6 +575,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter highTemperatureFilter = new KeyFilter(); KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "alarmActiveTime")); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "alarmActiveTime"));
highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate(); NumericFilterPredicate predicate = new NumericFilterPredicate();
DynamicValue<Double> dynamicValue = DynamicValue<Double> dynamicValue =
@ -627,16 +599,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
Awaitility.await() await()
.alias("data by query") .alias("data by query")
.atMost(TIMEOUT, TimeUnit.SECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {}); var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData()); var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == numOfDevices; return loadedEntities.size() == numOfDevices;
}); });
var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {}); var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData()); var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(numOfDevices, loadedEntities.size()); Assert.assertEquals(numOfDevices, loadedEntities.size());
@ -694,11 +666,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null); EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null);
PageData<EntityData> data = PageData<EntityData> data = findByQueryAndCheck(query, 97);
doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(97, data.getTotalElements());
Assert.assertEquals(10, data.getTotalPages()); Assert.assertEquals(10, data.getTotalPages());
Assert.assertTrue(data.hasNext()); Assert.assertTrue(data.hasNext());
Assert.assertEquals(10, data.getData().size()); Assert.assertEquals(10, data.getData().size());
@ -712,9 +680,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
}); });
EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter); EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter);
countByQueryAndCheck(countQuery, 97);
Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class);
Assert.assertEquals(97, count.longValue());
} }
@Test @Test
@ -742,28 +708,29 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30); KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30);
KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME);
KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName");
KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT");
KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER");
// all devices with ownerName = TEST TENANT // all devices with ownerName = TEST TENANT
EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter));
checkEntitiesCount(query, numOfDevices); await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query),
result -> result == numOfDevices);
// all devices with ownerName = TEST TENANT // all devices with ownerName = TEST TENANT
EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter)); EntityCountQuery activeAlarmTimeToLongQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeToLongFilter, tenantOwnerNameFilter));
checkEntitiesCount(activeAlarmTimeToLongQuery, 0); countByQueryAndCheck(activeAlarmTimeToLongQuery, 0);
// all devices with wrong ownerName // all devices with wrong ownerName
EntityCountQuery wrongTenantNameQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); EntityCountQuery wrongTenantNameQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter));
checkEntitiesCount(wrongTenantNameQuery, 0); countByQueryAndCheck(wrongTenantNameQuery, 0);
// all devices with owner type = TENANT // all devices with owner type = TENANT
EntityCountQuery tenantEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); EntityCountQuery tenantEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter));
checkEntitiesCount(tenantEntitiesQuery, numOfDevices); countByQueryAndCheck(tenantEntitiesQuery, numOfDevices);
// all devices with owner type = CUSTOMER // all devices with owner type = CUSTOMER
EntityCountQuery customerEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); EntityCountQuery customerEntitiesQuery = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter));
checkEntitiesCount(customerEntitiesQuery, 0); countByQueryAndCheck(customerEntitiesQuery, 0);
} }
@Test @Test
@ -790,7 +757,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5);
KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME);
KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName");
KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT");
KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER");
EntityDataSortOrder sortOrder = new EntityDataSortOrder( EntityDataSortOrder sortOrder = new EntityDataSortOrder(
@ -851,41 +818,29 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> data = findByQueryAndCheck(query, 1);
doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(1, data.getTotalElements());
Assert.assertEquals(1, data.getTotalPages());
Assert.assertEquals(1, data.getData().size());
// unnassign dashboard // unnassign dashboard
login(TENANT_EMAIL, TENANT_PASSWORD); login(TENANT_EMAIL, TENANT_PASSWORD);
doDelete("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); doDelete("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
PageData<EntityData> dataAfterUnassign = findByQueryAndCheck(query, 0);
doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {
});
Assert.assertEquals(0, dataAfterUnassign.getTotalElements());
Assert.assertEquals(0, dataAfterUnassign.getTotalPages());
Assert.assertEquals(0, dataAfterUnassign.getData().size());
} }
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception {
Awaitility.await() await()
.alias("data by query") .alias("data by query")
.atMost(30, TimeUnit.SECONDS) .atMost(30, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {}); var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData()); var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == expectedNumOfDevices; return loadedEntities.size() == expectedNumOfDevices;
}); });
if (expectedNumOfDevices == 0) { if (expectedNumOfDevices == 0) {
return; return;
} }
var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {}); var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData()); var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); 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(); String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue();
Assert.assertEquals("Device" + i, name); Assert.assertEquals("Device" + i, name);
Assert.assertEquals( expectedOwnerName, ownerName); Assert.assertEquals(expectedOwnerName, ownerName);
Assert.assertEquals( expectedOwnerType, ownerType); Assert.assertEquals(expectedOwnerType, ownerType);
Assert.assertEquals("1" + i, alarmActiveTime); Assert.assertEquals("1" + i, alarmActiveTime);
} }
} }
private void checkEntitiesCount(EntityCountQuery query, int expectedNumOfDevices) { protected PageData<EntityData> findByQuery(EntityDataQuery query) throws Exception {
Awaitility.await() return doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {
.alias("count by query") });
.atMost(30, TimeUnit.SECONDS) }
.until(() -> {
var count = doPost("/api/entitiesQuery/count", query, Integer.class); protected PageData<EntityData> findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) throws Exception {
return count == expectedNumOfDevices; PageData<EntityData> 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) { private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) {
KeyFilter tenantOwnerNameFilter = new KeyFilter(); KeyFilter tenantOwnerNameFilter = new KeyFilter();
tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName));
tenantOwnerNameFilter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate ownerNamePredicate = new StringFilterPredicate(); StringFilterPredicate ownerNamePredicate = new StringFilterPredicate();
ownerNamePredicate.setValue(FilterPredicateValue.fromString(value)); ownerNamePredicate.setValue(FilterPredicateValue.fromString(value));
ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL); ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL);
@ -927,6 +894,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) { private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) {
KeyFilter numericFilter = new KeyFilter(); KeyFilter numericFilter = new KeyFilter();
numericFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, attribute)); numericFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, attribute));
numericFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate(); NumericFilterPredicate predicate = new NumericFilterPredicate();
predicate.setValue(FilterPredicateValue.fromDouble(value)); predicate.setValue(FilterPredicateValue.fromDouble(value));
predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);

17
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++) { for (int queueIndex = 0; queueIndex < queueCount; queueIndex++) {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId);
for (int partition = 0; partition < partitionCount; partition++) { 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(); String serviceId = serviceInfo.getServiceId();
map.put(serviceId, map.get(serviceId) + 1); map.put(serviceId, map.get(serviceId) + 1);
} }
@ -308,7 +308,7 @@ public class HashPartitionServiceTest {
partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine)); partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine));
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TenantId.SYS_TENANT_ID); 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); Mockito.reset(applicationEventPublisher);
@ -336,14 +336,14 @@ public class HashPartitionServiceTest {
// expecting event about no partitions for isolated queue key // expecting event about no partitions for isolated queue key
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); 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.updateQueues(List.of(queueUpdateMsg));
partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine)); partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine));
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); 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)); partitionService_dedicated.removeQueues(List.of(queueDeleteMsg));
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); 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 -> { Stream.concat(Stream.of(TenantId.SYS_TENANT_ID), Stream.generate(UUID::randomUUID).map(TenantId::new).limit(10)).forEach(tenantId -> {
List<QueueKey> queues = Stream.generate(() -> RandomStringUtils.randomAlphabetic(10)) List<QueueKey> queues = Stream.generate(() -> RandomStringUtils.randomAlphabetic(10))
.map(queueName -> new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId)) .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++) { 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) { 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()) assertThat(assignedRuleEngine).as(queueKey + "[" + partition + "] should be assigned to " + expectedAssignedRuleEngine.getServiceId())
.isEqualTo(expectedAssignedRuleEngine); .isEqualTo(expectedAssignedRuleEngine);
} }
@ -434,6 +434,7 @@ public class HashPartitionServiceTest {
ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName); ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName);
ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge"); ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge");
ReflectionTestUtils.setField(partitionService, "edgePartitions", 10); ReflectionTestUtils.setField(partitionService, "edgePartitions", 10);
ReflectionTestUtils.setField(partitionService, "edqsPartitions", 12);
partitionService.init(); partitionService.init();
partitionService.partitionsInit(); partitionService.partitionsInit();
return partitionService; return partitionService;

4
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)); state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry));
Map<String, ArgumentEntry> newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); Map<String, ArgumentEntry> newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry);
boolean stateUpdated = state.updateState(newArgs); boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue(); assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@ -107,7 +107,7 @@ public class ScriptCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L);
Map<String, ArgumentEntry> newArgs = Map.of("assetHumidity", newArgEntry); Map<String, ArgumentEntry> newArgs = Map.of("assetHumidity", newArgEntry);
boolean stateUpdated = state.updateState(newArgs); boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue(); assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(

6
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -88,7 +88,7 @@ public class SimpleCalculatedFieldStateTest {
)); ));
Map<String, ArgumentEntry> newArgs = Map.of("key3", key3ArgEntry); Map<String, ArgumentEntry> newArgs = Map.of("key3", key3ArgEntry);
boolean stateUpdated = state.updateState(newArgs); boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue(); assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@ -106,7 +106,7 @@ public class SimpleCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L);
Map<String, ArgumentEntry> newArgs = Map.of("key1", newArgEntry); Map<String, ArgumentEntry> newArgs = Map.of("key1", newArgEntry);
boolean stateUpdated = state.updateState(newArgs); boolean stateUpdated = state.updateState(ctx, newArgs);
assertThat(stateUpdated).isTrue(); assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry));
@ -120,7 +120,7 @@ public class SimpleCalculatedFieldStateTest {
)); ));
Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
assertThatThrownBy(() -> state.updateState(newArgs)) assertThatThrownBy(() -> state.updateState(ctx, newArgs))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields."); .hasMessage("Rolling argument entry is not supported for simple calculated fields.");
} }

128
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<Asset> buildings = new ArrayList<>();
List<Asset> apartments = new ArrayList<>();
Map<String, Map<UUID, String>> entityNameByTypeMap = new HashMap<>();
Map<UUID, UUID> 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<EntityData> findByQueryAndCheck(CustomerId customerId, EntityDataQuery query, long expectedResultSize) {
return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findByQuery(customerId, query),
result -> result.getTotalElements() == expectedResultSize);
}
@Override
protected List<EntityData> findByQueryAndCheckTelemetry(EntityDataQuery query, EntityKeyType entityKeyType, String key, List<String> 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);
}
}

799
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java → application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

File diff suppressed because it is too large

93
application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java

@ -532,6 +532,98 @@ public class DefaultTbCoreConsumerServiceTest {
then(statsMock).should(never()).log(inactivityMsg); then(statsMock).should(never()).log(inactivityMsg);
} }
@Test
public void givenProcessingSuccess_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenOnSuccessCallbackIsCalled() {
// GIVEN
var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setInactivityTimeout(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// THEN
then(stateServiceMock).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, time);
then(tbCallbackMock).should().onSuccess();
then(tbCallbackMock).should(never()).onFailure(any());
}
@Test
public void givenProcessingFailure_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenOnFailureCallbackIsCalled() {
// GIVEN
var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setInactivityTimeout(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
var runtimeException = new RuntimeException("Something bad happened!");
doThrow(runtimeException).when(stateServiceMock).onDeviceInactivityTimeoutUpdate(tenantId, deviceId, time);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// THEN
then(tbCallbackMock).should(never()).onSuccess();
then(tbCallbackMock).should().onFailure(runtimeException);
}
@Test
public void givenStatsEnabled_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenStatsAreRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true);
var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setInactivityTimeout(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// THEN
then(statsMock).should().log(inactivityTimeoutUpdateMsg);
}
@Test
public void givenStatsDisabled_whenForwardingInactivityTimeoutUpdateMsgToStateService_thenStatsAreNotRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false);
var inactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setInactivityTimeout(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityTimeoutUpdateMsg, tbCallbackMock);
// THEN
then(statsMock).should(never()).log(inactivityTimeoutUpdateMsg);
}
@Test @Test
public void givenRestApiCallResponseMsgProto_whenForwardToRuleEngineCallService_thenCallOnQueueMsg() { public void givenRestApiCallResponseMsgProto_whenForwardToRuleEngineCallService_thenCallOnQueueMsg() {
// GIVEN // GIVEN
@ -545,4 +637,5 @@ public class DefaultTbCoreConsumerServiceTest {
// THEN // THEN
then(ruleEngineCallServiceMock).should().onQueueMsg(restApiCallResponseMsgProto, tbCallbackMock); then(ruleEngineCallServiceMock).should().onQueueMsg(restApiCallResponseMsgProto, tbCallbackMock);
} }
} }

11
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java

@ -626,8 +626,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
.until(() -> consumer.subscribed && consumer.getPartitions().equals(expectedPartitions) && consumer.pollingStarted); .until(() -> consumer.subscribed && consumer.getPartitions().equals(expectedPartitions) && consumer.pollingStarted);
verify(consumer, times(1)).subscribe(any()); verify(consumer, times(1)).subscribe(any());
verify(consumer).subscribe(eq(expectedPartitions)); verify(consumer).subscribe(eq(expectedPartitions));
verify(consumer).doSubscribe(argThat(topics -> topics.containsAll(expectedPartitions.stream() verify(consumer).doSubscribe(argThat(topics -> topics.containsAll(expectedPartitions)));
.map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()))));
verify(consumer, atLeastOnce()).poll(eq((long) queue.getPollInterval())); verify(consumer, atLeastOnce()).poll(eq((long) queue.getPollInterval()));
verify(consumer, atLeastOnce()).doPoll(eq((long) queue.getPollInterval())); verify(consumer, atLeastOnce()).doPoll(eq((long) queue.getPollInterval()));
verify(consumer, never()).unsubscribe(); verify(consumer, never()).unsubscribe();
@ -743,9 +742,11 @@ public class TbRuleEngineQueueConsumerManagerTest {
} }
@Override @Override
protected void doSubscribe(List<String> topicNames) { protected void doSubscribe(Set<TopicPartitionInfo> partitions) {
log.debug("doSubscribe({})", topicNames); this.topics = partitions.stream()
this.topics = topicNames; .map(TopicPartitionInfo::getFullTopicName)
.collect(Collectors.toList());
log.debug("doSubscribe({})", topics);
subscribed = true; subscribed = true;
} }

61
application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java → application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateManagerTest.java

@ -52,7 +52,7 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class DefaultRuleEngineDeviceStateManagerTest { public class DefaultDeviceStateManagerTest {
@Mock @Mock
private DeviceStateService deviceStateServiceMock; private DeviceStateService deviceStateServiceMock;
@ -71,7 +71,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
@Captor @Captor
private ArgumentCaptor<TbQueueCallback> queueCallbackCaptor; private ArgumentCaptor<TbQueueCallback> queueCallbackCaptor;
private DefaultRuleEngineDeviceStateManager deviceStateManager; private DefaultDeviceStateManager deviceStateManager;
private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002"));
private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002");
@ -82,7 +82,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
@BeforeEach @BeforeEach
public void setup() { public void setup() {
deviceStateManager = new DefaultRuleEngineDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock); deviceStateManager = new DefaultDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock);
} }
@ParameterizedTest @ParameterizedTest
@ -90,7 +90,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
"when onDeviceX() is called, then should route event to local service and call onSuccess() callback.") "when onDeviceX() is called, then should route event to local service and call onSuccess() callback.")
@MethodSource @MethodSource
public void givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback( public void givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback(
BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback> onDeviceAction, Consumer<DeviceStateService> actionVerification BiConsumer<DefaultDeviceStateManager, TbCallback> onDeviceAction, Consumer<DeviceStateService> actionVerification
) { ) {
// GIVEN // GIVEN
given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true);
@ -109,20 +109,24 @@ public class DefaultRuleEngineDeviceStateManagerTest {
private static Stream<Arguments> givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback() { private static Stream<Arguments> givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback() {
return Stream.of( return Stream.of(
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS)
), ),
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS)
), ),
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS)
), ),
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS)
) )
); );
} }
@ -132,7 +136,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
"when onDeviceX() is called, then should route event to local service and call onFailure() callback.") "when onDeviceX() is called, then should route event to local service and call onFailure() callback.")
@MethodSource @MethodSource
public void givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback( public void givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback(
Consumer<DeviceStateService> exceptionThrowSetup, BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback> onDeviceAction, Consumer<DeviceStateService> actionVerification Consumer<DeviceStateService> exceptionThrowSetup, BiConsumer<DefaultDeviceStateManager, TbCallback> onDeviceAction, Consumer<DeviceStateService> actionVerification
) { ) {
// GIVEN // GIVEN
given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true);
@ -155,23 +159,28 @@ public class DefaultRuleEngineDeviceStateManagerTest {
return Stream.of( return Stream.of(
Arguments.of( Arguments.of(
(Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS), (Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS),
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS)
), ),
Arguments.of( Arguments.of(
(Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS), (Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS),
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS)
), ),
Arguments.of( Arguments.of(
(Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS), (Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS),
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS)
), ),
Arguments.of( Arguments.of(
(Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), (Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS),
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) (Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Consumer<DeviceStateService>) deviceStateServiceMock -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS),
(BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Consumer<DeviceStateService>) deviceStateServiceMock -> then(deviceStateServiceMock).should().onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS)
) )
); );
} }
@ -181,7 +190,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
"when onDeviceX() is called, then should send correct queue message to external service with correct callback object.") "when onDeviceX() is called, then should send correct queue message to external service with correct callback object.")
@MethodSource @MethodSource
public void givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback( public void givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback(
BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback> onDeviceAction, BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>> actionVerification BiConsumer<DefaultDeviceStateManager, TbCallback> onDeviceAction, BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>> actionVerification
) { ) {
// WHEN // WHEN
ReflectionTestUtils.setField(deviceStateManager, "deviceStateService", Optional.empty()); ReflectionTestUtils.setField(deviceStateManager, "deviceStateService", Optional.empty());
@ -203,7 +212,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
private static Stream<Arguments> givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback() { private static Stream<Arguments> givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback() {
return Stream.of( return Stream.of(
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> { (BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> {
var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
@ -219,7 +228,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
} }
), ),
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> { (BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> {
var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
@ -235,7 +244,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
} }
), ),
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> { (BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> {
var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
@ -251,7 +260,7 @@ public class DefaultRuleEngineDeviceStateManagerTest {
} }
), ),
Arguments.of( Arguments.of(
(BiConsumer<DefaultRuleEngineDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), (BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> { (BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> {
var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
@ -265,6 +274,22 @@ public class DefaultRuleEngineDeviceStateManagerTest {
.build(); .build();
then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture());
} }
),
Arguments.of(
(BiConsumer<DefaultDeviceStateManager, TbCallback>) (deviceStateManager, tbCallbackMock) -> deviceStateManager.onDeviceInactivityTimeoutUpdate(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(BiConsumer<TbClusterService, ArgumentCaptor<TbQueueCallback>>) (clusterServiceMock, queueCallbackCaptor) -> {
var deviceInactivityTimeoutUpdateMsg = TransportProtos.DeviceInactivityTimeoutUpdateProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits())
.setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits())
.setInactivityTimeout(EVENT_TS)
.build();
var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setDeviceInactivityTimeoutUpdateMsg(deviceInactivityTimeoutUpdateMsg)
.build();
then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture());
}
) )
); );
} }

66
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -38,9 +38,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
@ -72,7 +69,7 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -88,7 +85,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_ALARM_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_ALARM_TIME;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_ACTIVITY_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_ACTIVITY_TIME;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME;
@ -211,7 +207,7 @@ public class DefaultDeviceStateServiceTest {
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(LAST_CONNECT_TIME) && request.getEntries().get(0).getKey().equals(LAST_CONNECT_TIME) &&
request.getEntries().get(0).getValue().equals(lastConnectTime) request.getEntries().get(0).getValue().equals(lastConnectTime)
@ -298,7 +294,7 @@ public class DefaultDeviceStateServiceTest {
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(LAST_DISCONNECT_TIME) && request.getEntries().get(0).getKey().equals(LAST_DISCONNECT_TIME) &&
request.getEntries().get(0).getValue().equals(lastDisconnectTime) request.getEntries().get(0).getValue().equals(lastDisconnectTime)
@ -421,13 +417,13 @@ public class DefaultDeviceStateServiceTest {
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) &&
request.getEntries().get(0).getValue().equals(lastInactivityTime) request.getEntries().get(0).getValue().equals(lastInactivityTime)
)); ));
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(false) request.getEntries().get(0).getValue().equals(false)
@ -465,12 +461,12 @@ public class DefaultDeviceStateServiceTest {
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME)
)); ));
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(false) request.getEntries().get(0).getValue().equals(false)
@ -508,42 +504,6 @@ public class DefaultDeviceStateServiceTest {
verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId);
} }
@Test
public void givenPersistToTelemetryAndDefaultInactivityTimeoutFetched_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() {
var defaultInactivityTimeoutInSec = 60L;
var latest =
Map.of(
EntityKeyType.TIME_SERIES, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(defaultInactivityTimeoutInSec * 1000))),
EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L)))
);
process(latest, defaultInactivityTimeoutInSec);
}
@Test
public void givenPersistToTelemetryAndNoInactivityTimeoutFetchedFromTimeSeries_whenTransformingToDeviceStateData_thenTryGetInactivityFromAttribute() {
var defaultInactivityTimeoutInSec = 60L;
var latest =
Map.of(
EntityKeyType.SERVER_ATTRIBUTE, Map.of(INACTIVITY_TIMEOUT, new TsValue(0, Long.toString(5000L)))
);
process(latest, defaultInactivityTimeoutInSec);
}
private void process(Map<EntityKeyType, Map<String, TsValue>> latest, long defaultInactivityTimeoutInSec) {
service.setDefaultInactivityTimeoutInSec(defaultInactivityTimeoutInSec);
service.setDefaultInactivityTimeoutMs(defaultInactivityTimeoutInSec * 1000);
service.setPersistToTelemetry(true);
var deviceUuid = UUID.randomUUID();
var deviceId = new DeviceId(deviceUuid);
DeviceStateData deviceStateData = service.toDeviceStateData(new EntityData(deviceId, latest, Map.of()), new DeviceIdInfo(TenantId.SYS_TENANT_ID.getId(), UUID.randomUUID(), deviceUuid));
assertThat(deviceStateData.getState().getInactivityTimeout()).isEqualTo(5000L);
}
private void initStateService(long timeout) throws InterruptedException { private void initStateService(long timeout) throws InterruptedException {
service.stop(); service.stop();
reset(service, telemetrySubscriptionService); reset(service, telemetrySubscriptionService);
@ -556,7 +516,7 @@ public class DefaultDeviceStateServiceTest {
.thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false)); .thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false));
PartitionChangeEvent event = new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of( PartitionChangeEvent event = new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(
new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi) new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi)
)); ), Collections.emptyMap());
service.onApplicationEvent(event); service.onApplicationEvent(event);
Thread.sleep(100); Thread.sleep(100);
} }
@ -1002,7 +962,7 @@ public class DefaultDeviceStateServiceTest {
assertThat(actualNotification.isActive()).isFalse(); assertThat(actualNotification.isActive()).isFalse();
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getTenantId().equals(TenantId.SYS_TENANT_ID) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) &&
request.getEntries().get(0).getValue().equals(expectedLastInactivityAlarmTime) request.getEntries().get(0).getValue().equals(expectedLastInactivityAlarmTime)
@ -1114,7 +1074,7 @@ public class DefaultDeviceStateServiceTest {
final long defaultTimeout = 1000; final long defaultTimeout = 1000;
initStateService(defaultTimeout); initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList()));
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
@ -1170,7 +1130,7 @@ public class DefaultDeviceStateServiceTest {
assertThat(attributeRequestCaptor.getAllValues()).hasSize(2) assertThat(attributeRequestCaptor.getAllValues()).hasSize(2)
.anySatisfy(request -> { .anySatisfy(request -> {
assertThat(request.getTenantId()).isEqualTo(TenantId.SYS_TENANT_ID); assertThat(request.getTenantId()).isEqualTo(tenantId);
assertThat(request.getEntityId()).isEqualTo(deviceId); assertThat(request.getEntityId()).isEqualTo(deviceId);
assertThat(request.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); assertThat(request.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE);
assertThat(request.getEntries()).singleElement().satisfies(attributeKvEntry -> { assertThat(request.getEntries()).singleElement().satisfies(attributeKvEntry -> {
@ -1179,7 +1139,7 @@ public class DefaultDeviceStateServiceTest {
}); });
}) })
.anySatisfy(request -> { .anySatisfy(request -> {
assertThat(request.getTenantId()).isEqualTo(TenantId.SYS_TENANT_ID); assertThat(request.getTenantId()).isEqualTo(tenantId);
assertThat(request.getEntityId()).isEqualTo(deviceId); assertThat(request.getEntityId()).isEqualTo(deviceId);
assertThat(request.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); assertThat(request.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE);
assertThat(request.getEntries()).singleElement().satisfies(attributeKvEntry -> { assertThat(request.getEntries()).singleElement().satisfies(attributeKvEntry -> {
@ -1196,7 +1156,7 @@ public class DefaultDeviceStateServiceTest {
final long defaultTimeout = 1000; final long defaultTimeout = 1000;
initStateService(defaultTimeout); initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList()));
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder() DeviceState deviceState = DeviceState.builder()

28
application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -74,6 +75,7 @@ import java.util.stream.Stream;
import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -159,7 +161,7 @@ class DefaultTelemetrySubscriptionServiceTest {
}).when(calculatedFieldQueueService).pushRequestToQueue(any(TimeseriesSaveRequest.class), any(), any()); }).when(calculatedFieldQueueService).pushRequestToQueue(any(TimeseriesSaveRequest.class), any(), any());
// send partition change event so currentPartitions set is populated // send partition change event so currentPartitions set is populated
telemetryService.onTbApplicationEvent(new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(new QueueKey(ServiceType.TB_CORE), Set.of(tpi)))); telemetryService.onTbApplicationEvent(new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(new QueueKey(ServiceType.TB_CORE), Set.of(tpi)), Collections.emptyMap()));
} }
@AfterEach @AfterEach
@ -168,6 +170,28 @@ class DefaultTelemetrySubscriptionServiceTest {
tsCallBackExecutor.shutdownNow(); tsCallBackExecutor.shutdownNow();
} }
/* --- Save time series API --- */
@Test
void shouldThrowErrorWhenTryingToSaveTimeseriesForApiUsageState() {
// GIVEN
var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId)
.customerId(customerId)
.entityId(new ApiUsageStateId(UUID.randomUUID()))
.entries(sampleTelemetry)
.strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL)
.build();
// WHEN
assertThatThrownBy(() -> telemetryService.saveTimeseries(request))
.isInstanceOf(RuntimeException.class)
.hasMessage("Can't update API Usage State!");
// THEN
then(tsService).shouldHaveNoInteractions();
}
@Test @Test
void shouldReportStorageDataPointsApiUsageWhenTimeSeriesIsSaved() { void shouldReportStorageDataPointsApiUsageWhenTimeSeriesIsSaved() {
// GIVEN // GIVEN
@ -377,7 +401,7 @@ class DefaultTelemetrySubscriptionServiceTest {
); );
} }
// used to emulate versions returned by save latest API // used to emulate sequence numbers returned by save latest API
private static List<Long> listOfNNumbers(int N) { private static List<Long> listOfNNumbers(int N) {
return LongStream.range(0, N).boxed().toList(); return LongStream.range(0, N).boxed().toList();
} }

3
application/src/test/resources/application-test.properties

@ -58,3 +58,6 @@ sql.ttl.edge_events.edge_event_ttl=2592000
server.log_controller_error_stack_trace=false server.log_controller_error_stack_trace=false
transport.gateway.dashboard.sync.enabled=false transport.gateway.dashboard.sync.enabled=false
queue.edqs.sync.enabled=false
queue.edqs.api.supported=false

6
application/src/test/resources/update/330/device_profile_001_out.json

@ -64,7 +64,8 @@
"dynamicValue": { "dynamicValue": {
"sourceType": null, "sourceType": null,
"sourceAttribute": null, "sourceAttribute": null,
"inherit": false "inherit": false,
"resolvedValue" : null
} }
} }
} }
@ -103,7 +104,8 @@
"dynamicValue": { "dynamicValue": {
"sourceType": null, "sourceType": null,
"sourceAttribute": null, "sourceAttribute": null,
"inherit": false "inherit": false,
"resolvedValue" : null
} }
} }
} }

2
common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java

@ -26,6 +26,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext
ListenableFuture<Response> send(Request request, long timeoutNs); ListenableFuture<Response> send(Request request, long timeoutNs);
ListenableFuture<Response> send(Request request, Integer partition);
void stop(); void stop();
void setMessagesStats(MessagesStats messagesStats); void setMessagesStats(MessagesStats messagesStats);

10
common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueResponseTemplate.java

@ -15,9 +15,17 @@
*/ */
package org.thingsboard.server.queue; package org.thingsboard.server.queue;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import java.util.Set;
public interface TbQueueResponseTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> { public interface TbQueueResponseTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> {
void init(TbQueueHandler<Request, Response> handler); void subscribe();
void subscribe(Set<TopicPartitionInfo> partitions);
void launch(TbQueueHandler<Request, Response> handler);
void stop(); void stop();
} }

89
common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java

@ -0,0 +1,89 @@
/**
* 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.common.data;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
public enum ObjectType {
TENANT,
TENANT_PROFILE,
CUSTOMER,
QUEUE,
RPC,
RULE_CHAIN,
OTA_PACKAGE,
RESOURCE,
EVENT,
RULE_NODE,
USER,
EDGE,
WIDGETS_BUNDLE,
WIDGET_TYPE,
DASHBOARD,
DEVICE_PROFILE,
DEVICE,
DEVICE_CREDENTIALS,
ASSET_PROFILE,
ASSET,
ENTITY_VIEW,
ALARM,
ENTITY_ALARM,
OAUTH2_CLIENT,
OAUTH2_DOMAIN,
OAUTH2_MOBILE,
USER_SETTINGS,
NOTIFICATION_TARGET,
NOTIFICATION_TEMPLATE,
NOTIFICATION_RULE,
ALARM_COMMENT,
API_USAGE_STATE,
QUEUE_STATS,
AUDIT_LOG,
RELATION,
ATTRIBUTE_KV,
LATEST_TS_KV;
public static final Set<ObjectType> edqsTenantTypes = EnumSet.of(
TENANT, CUSTOMER, DEVICE_PROFILE, DEVICE, ASSET_PROFILE, ASSET, EDGE, ENTITY_VIEW, USER, DASHBOARD,
RULE_CHAIN, WIDGET_TYPE, WIDGETS_BUNDLE, API_USAGE_STATE, QUEUE_STATS
);
public static final Set<ObjectType> edqsTypes = EnumSet.copyOf(edqsTenantTypes);
public static final Set<ObjectType> edqsSystemTypes = EnumSet.of(TENANT, USER, DASHBOARD,
API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV);
public static final Set<ObjectType> unversionedTypes = EnumSet.of(
QUEUE_STATS // created once, never updated
);
static {
edqsTypes.addAll(List.of(RELATION, ATTRIBUTE_KV, LATEST_TS_KV));
}
public EntityType toEntityType() {
return EntityType.valueOf(name());
}
public static ObjectType fromEntityType(EntityType entityType) {
try {
return ObjectType.valueOf(entityType.name());
} catch (Exception e) {
return null;
}
}
}

1
common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java

@ -19,7 +19,6 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;

2
common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -58,6 +59,7 @@ public class TenantProfile extends BaseData<TenantProfileId> implements HasName
@Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + @Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " +
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false") "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false")
private boolean isolatedTbRuleEngine; private boolean isolatedTbRuleEngine;
@Valid
@Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") @Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.")
private transient TenantProfileData profileData; private transient TenantProfileData profileData;
@JsonIgnore @JsonIgnore

75
common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java

@ -0,0 +1,75 @@
/**
* 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.common.data.edqs;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AttributeKv implements EdqsObject {
private EntityId entityId;
private AttributeScope scope;
private String key;
private Long version;
private DataPoint dataPoint; // optional (on deletion)
private Long lastUpdateTs; // only for serialization
private KvEntry value; // only for serialization
public AttributeKv(EntityId entityId, AttributeScope scope, AttributeKvEntry attributeKvEntry, long version) {
this.entityId = entityId;
this.scope = scope;
this.key = attributeKvEntry.getKey();
this.version = version;
this.lastUpdateTs = attributeKvEntry.getLastUpdateTs();
this.value = attributeKvEntry;
}
public AttributeKv(EntityId entityId, AttributeScope scope, String key, long version) {
this.entityId = entityId;
this.scope = scope;
this.key = key;
this.version = version;
}
@Override
public String key() {
return "a_" + entityId + "_" + scope + "_" + key;
}
@Override
public Long version() {
return version;
}
@Override
public ObjectType type() {
return ObjectType.ATTRIBUTE_KV;
}
}

40
common/data/src/main/java/org/thingsboard/server/common/data/edqs/DataPoint.java

@ -0,0 +1,40 @@
/**
* 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.common.data.edqs;
import org.thingsboard.server.common.data.kv.DataType;
public interface DataPoint {
String NOT_SUPPORTED = "Not supported!";
long getTs();
DataType getType();
String getStr();
long getLong();
double getDouble();
boolean getBool();
String getJson();
String valueToString();
}

34
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEvent.java

@ -0,0 +1,34 @@
/**
* 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.common.data.edqs;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@AllArgsConstructor
@Builder
public class EdqsEvent {
private final TenantId tenantId;
private final ObjectType objectType;
private final EdqsEventType eventType;
private final EdqsObject object;
}

21
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsEventType.java

@ -0,0 +1,21 @@
/**
* 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.common.data.edqs;
public enum EdqsEventType {
UPDATED,
DELETED
}

32
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java

@ -0,0 +1,32 @@
/**
* 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.common.data.edqs;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.thingsboard.server.common.data.ObjectType;
public interface EdqsObject {
@JsonIgnore
String key();
@JsonIgnore
Long version();
@JsonIgnore
ObjectType type();
}

24
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsSyncRequest.java

@ -0,0 +1,24 @@
/**
* 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.common.data.edqs;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties
public class EdqsSyncRequest {
}

68
common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java

@ -0,0 +1,68 @@
/**
* 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.common.data.edqs;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.edqs.fields.EntityFields;
import org.thingsboard.server.common.data.edqs.fields.EntityIdFields;
import java.util.UUID;
@Data
@NoArgsConstructor
public class Entity implements EdqsObject {
private EntityType type;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
private EntityFields fields;
public Entity(EntityType type) {
this.type = type;
}
public Entity(EntityType type, EntityFields fields) {
this.type = type;
this.fields = fields;
}
public Entity(EntityType entityType, UUID id, long version) {
this.type = entityType;
this.fields = new EntityIdFields(id, version);
}
@Override
public String key() {
return "e_" + fields.getId().toString();
}
@Override
public Long version() {
return fields.getVersion();
}
@Override
public ObjectType type() {
return ObjectType.fromEntityType(type);
}
}

70
common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java

@ -0,0 +1,70 @@
/**
* 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.common.data.edqs;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LatestTsKv implements EdqsObject {
private EntityId entityId;
private String key;
private Long version;
private DataPoint dataPoint; // optional (on deletion)
private Long ts; // only for serialization
private KvEntry value; // only for serialization
public LatestTsKv(EntityId entityId, TsKvEntry tsKvEntry, Long version) {
this.entityId = entityId;
this.key = tsKvEntry.getKey();
this.ts = tsKvEntry.getTs();
this.version = version != null ? version : 0L;
this.value = tsKvEntry;
}
public LatestTsKv(EntityId entityId, String key, Long version) {
this.entityId = entityId;
this.key = key;
this.version = version != null ? version : 0L;
}
public String key() {
return "l_" + entityId + "_" + key;
}
@Override
public Long version() {
return version;
}
@Override
public ObjectType type() {
return ObjectType.LATEST_TS_KV;
}
}

32
common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java

@ -0,0 +1,32 @@
/**
* 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.common.data.edqs;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ToCoreEdqsMsg {
private EdqsSyncRequest syncRequest;
private Boolean apiEnabled;
}

38
common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java

@ -0,0 +1,38 @@
/**
* 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.common.data.edqs;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ToCoreEdqsRequest {
private EdqsSyncRequest syncRequest;
private Boolean apiEnabled;
@JsonIgnore
public ToCoreEdqsMsg toInternalMsg() {
return new ToCoreEdqsMsg(syncRequest, apiEnabled);
}
}

65
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java

@ -0,0 +1,65 @@
/**
* 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.common.data.edqs.fields;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.id.CustomerId;
import java.util.UUID;
@Data
@SuperBuilder
public class AbstractEntityFields implements EntityFields {
private UUID id;
private long createdTime;
private UUID tenantId;
private UUID customerId;
private String name;
private Long version;
public AbstractEntityFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version) {
this.id = id;
this.createdTime = createdTime;
this.tenantId = tenantId;
this.customerId = (customerId != null && customerId != CustomerId.NULL_UUID) ? customerId : null;
this.name = name;
this.version = version;
}
public AbstractEntityFields() {
}
public AbstractEntityFields(UUID id, long createdTime, UUID tenantId, String name, Long version) {
this(id, createdTime, tenantId, null, name, version);
}
public AbstractEntityFields(UUID id, long createdTime, UUID tenantId, UUID customerId, Long version) {
this(id, createdTime, tenantId, customerId, null, version);
}
public AbstractEntityFields(UUID id, long createdTime, String name, Long version) {
this(id, createdTime, null, name, version);
}
public AbstractEntityFields(UUID id, long createdTime, UUID tenantId) {
this(id, createdTime, tenantId, null, null, null);
}
}

59
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ApiUsageStateFields.java

@ -0,0 +1,59 @@
/**
* 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.common.data.edqs.fields;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@SuperBuilder
public class ApiUsageStateFields extends AbstractEntityFields {
private EntityId entityId;
private ApiUsageStateValue transportState;
private ApiUsageStateValue dbStorageState;
private ApiUsageStateValue reExecState;
private ApiUsageStateValue jsExecState;
private ApiUsageStateValue tbelExecState;
private ApiUsageStateValue emailExecState;
private ApiUsageStateValue smsExecState;
private ApiUsageStateValue alarmExecState;
public ApiUsageStateFields(UUID id, long createdTime, UUID tenantId, UUID entityId, String entityType, ApiUsageStateValue transportState, ApiUsageStateValue dbStorageState,
ApiUsageStateValue reExecState, ApiUsageStateValue jsExecState, ApiUsageStateValue tbelExecState,
ApiUsageStateValue emailExecState, ApiUsageStateValue smsExecState, ApiUsageStateValue alarmExecState,
Long version) {
super(id, createdTime, tenantId, null, null, version);
this.entityId = (entityType != null && entityId != null) ? EntityIdFactory.getByTypeAndUuid(entityType, entityId) : null;
this.transportState = transportState;
this.dbStorageState = dbStorageState;
this.reExecState = reExecState;
this.jsExecState = jsExecState;
this.tbelExecState = tbelExecState;
this.emailExecState = emailExecState;
this.smsExecState = smsExecState;
this.alarmExecState = alarmExecState;
}
}

58
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetFields.java

@ -0,0 +1,58 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class AssetFields extends AbstractEntityFields implements ProfileAwareFields {
private String type;
private UUID assetProfileId;
private String label;
private String additionalInfo;
@JsonIgnore
@Override
public String getProfileName() {
return type;
}
@JsonIgnore
@Override
public UUID getProfileId() {
return assetProfileId;
}
public AssetFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name,
Long version, String type, String label, UUID assetProfileId, JsonNode additionalInfo) {
super(id, createdTime, tenantId, customerId, name, version);
this.type = type;
this.assetProfileId = assetProfileId;
this.label = label;
this.additionalInfo = getText(additionalInfo);
}
}

35
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AssetProfileFields.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.common.data.edqs.fields;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class AssetProfileFields extends AbstractEntityFields {
private boolean isDefault;
public AssetProfileFields(UUID id, long createdTime, UUID tenantId, String name, Long version, boolean isDefault) {
super(id, createdTime, tenantId, null, name, version);
this.isDefault = isDefault;
}
}

55
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/CustomerFields.java

@ -0,0 +1,55 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class CustomerFields extends AbstractEntityFields {
private String additionalInfo;
private String country;
private String state;
private String city;
private String address;
private String address2;
private String zip;
private String phone;
private String email;
public CustomerFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo,
String country, String state, String city, String address, String address2, String zip, String phone, String email) {
super(id, createdTime, tenantId, name, version);
this.additionalInfo = getText(additionalInfo);
this.country = country;
this.state = state;
this.city = city;
this.address = address;
this.address2 = address2;
this.zip = zip;
this.phone = phone;
this.email = email;
}
}

61
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DashboardFields.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.common.data.edqs.fields;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class DashboardFields extends AbstractEntityFields {
private static ObjectMapper objectMapper = new ObjectMapper();
private List<UUID> assignedCustomerIds;
public DashboardFields(UUID id, long createdTime, UUID tenantId, String assignedCustomers, String name, Long version) {
super(id, createdTime, tenantId, name, version);
this.assignedCustomerIds = getCustomerIds(assignedCustomers);
}
private static List<UUID> getCustomerIds(String assignedCustomers) {
List<UUID> ids = new ArrayList<>();
if (assignedCustomers == null || assignedCustomers.isEmpty()) {
return ids;
}
try {
JsonNode rootNode = objectMapper.readTree(assignedCustomers);
for (JsonNode node : rootNode) {
String idStr = node.path("customerId").path("id").asText();
if (!idStr.isEmpty()) {
ids.add(UUID.fromString(idStr));
}
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return ids;
}
}

58
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceFields.java

@ -0,0 +1,58 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class DeviceFields extends AbstractEntityFields implements ProfileAwareFields {
private String label;
private String type;
private UUID deviceProfileId;
private String additionalInfo;
@JsonIgnore
@Override
public String getProfileName() {
return type;
}
@JsonIgnore
@Override
public UUID getProfileId() {
return deviceProfileId;
}
public DeviceFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version, String type,
String label, UUID deviceProfileId, JsonNode additionalInfo) {
super(id, createdTime, tenantId, customerId, name, version);
this.label = label;
this.type = type;
this.deviceProfileId = deviceProfileId;
this.additionalInfo = getText(additionalInfo);
}
}

38
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/DeviceProfileFields.java

@ -0,0 +1,38 @@
/**
* 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.common.data.edqs.fields;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.DeviceProfileType;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class DeviceProfileFields extends AbstractEntityFields {
private String type;
private boolean isDefault;
public DeviceProfileFields(UUID id, long createdTime, UUID tenantId, String name, Long version, DeviceProfileType type, boolean isDefault) {
super(id, createdTime, tenantId, null, name, version);
this.type = type.name();
this.isDefault = isDefault;
}
}

43
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EdgeFields.java

@ -0,0 +1,43 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class EdgeFields extends AbstractEntityFields {
private String type;
private String label;
private String additionalInfo;
public EdgeFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, Long version,
String type, String label, JsonNode additionalInfo) {
super(id, createdTime, tenantId, customerId, name, version);
this.type = type;
this.label = label;
this.additionalInfo = getText(additionalInfo);
}
}

179
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java

@ -0,0 +1,179 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@JsonInclude(JsonInclude.Include.NON_NULL)
public interface EntityFields {
Logger log = LoggerFactory.getLogger(EntityFields.class);
default UUID getId() {
return null;
}
default UUID getTenantId() {
return null;
}
default UUID getCustomerId() {
return null;
}
default List<UUID> getAssignedCustomerIds() {
return Collections.emptyList();
}
default long getCreatedTime() {
return 0;
}
default String getName() {
return "";
}
default String getType() {
return "";
}
default String getLabel() {
return "";
}
default String getAdditionalInfo() {
return "";
}
default String getEmail() {
return "";
}
default String getCountry() {
return "";
}
default String getState() {
return "";
}
default String getCity() {
return "";
}
default String getAddress() {
return "";
}
default String getAddress2() {
return "";
}
default String getZip() {
return "";
}
default String getPhone() {
return "";
}
default String getRegion() {
return "";
}
default String getFirstName() {
return "";
}
default String getLastName() {
return "";
}
default boolean isEdgeTemplate() {
return false;
}
default String getConfiguration() {
return "";
}
default String getSchedule() {
return "";
}
default EntityId getOriginatorId() {
return null;
}
default String getQueueName() {
return "";
}
default String getServiceId() {
return "";
}
default boolean isDefault() {
return false;
}
default UUID getOwnerId() {
return null;
}
default Long getVersion() {
return null;
}
default String getAsString(String key) {
return switch (key) {
case "createdTime" -> Long.toString(getCreatedTime());
case "type" -> getType();
case "label" -> getLabel();
case "additionalInfo" -> getAdditionalInfo();
case "email" -> getEmail();
case "country" -> getCountry();
case "state" -> getState();
case "city" -> getCity();
case "address" -> getAddress();
case "address2" -> getAddress2();
case "zip" -> getZip();
case "phone" -> getPhone();
case "region" -> getRegion();
case "firstName" -> getFirstName();
case "lastName" -> getLastName();
case "edgeTemplate" -> Boolean.toString(isEdgeTemplate());
case "configuration" -> getConfiguration();
case "schedule" -> getSchedule();
case "originatorId" -> getOriginatorId().getId().toString();
case "originatorType" -> getOriginatorId().getEntityType().toString();
case "queueName" -> getQueueName();
case "serviceId" -> getServiceId();
default -> {
log.warn("Unknown field '{}'", key);
yield null;
}
};
}
}

36
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityIdFields.java

@ -0,0 +1,36 @@
/**
* 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.common.data.edqs.fields;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class EntityIdFields implements EntityFields {
private UUID id;
private Long version;
public EntityIdFields(UUID id, Long version) {
this.id = id;
this.version = version;
}
}

37
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java

@ -0,0 +1,37 @@
/**
* 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.common.data.edqs.fields;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class EntityViewFields extends AbstractEntityFields {
private String type;
private String additionalInfo;
public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, String additionalInfo, Long version) {
super(id, createdTime, tenantId, customerId, name, version);
this.type = type;
this.additionalInfo = additionalInfo;
}
}

299
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java

@ -0,0 +1,299 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.util.UUID;
public class FieldsUtil {
public static EntityFields toFields(Object entity) {
if (entity instanceof Customer customer) {
return toFields(customer);
} else if (entity instanceof Tenant tenant) {
return toFields(tenant);
} else if (entity instanceof TenantProfile tenantProfile) {
return toFields(tenantProfile);
} else if (entity instanceof Device device) {
return toFields(device);
} else if (entity instanceof Asset asset) {
return toFields(asset);
} else if (entity instanceof Edge edge) {
return toFields(edge);
} else if (entity instanceof EntityView entityView) {
return toFields(entityView);
} else if (entity instanceof User user) {
return toFields(user);
} else if (entity instanceof Dashboard dashboard) {
return toFields(dashboard);
} else if (entity instanceof RuleChain ruleChain) {
return toFields(ruleChain);
} else if (entity instanceof RuleNode ruleNode) {
return toFields(ruleNode);
} else if (entity instanceof WidgetType widgetType) {
return toFields(widgetType);
} else if (entity instanceof WidgetsBundle widgetsBundle) {
return toFields(widgetsBundle);
} else if (entity instanceof DeviceProfile deviceProfile) {
return toFields(deviceProfile);
} else if (entity instanceof AssetProfile assetProfile) {
return toFields(assetProfile);
} else if (entity instanceof QueueStats queueStats) {
return toFields(queueStats);
} else if (entity instanceof ApiUsageState apiUsageState) {
return toFields(apiUsageState);
} else {
throw new IllegalArgumentException("Unsupported entity type: " + entity.getClass().getName());
}
}
private static CustomerFields toFields(Customer entity) {
return CustomerFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getTitle())
.additionalInfo(getText(entity.getAdditionalInfo()))
.email(entity.getEmail())
.country(entity.getCountry())
.state(entity.getState())
.city(entity.getCity())
.address(entity.getAddress())
.address2(entity.getAddress2())
.zip(entity.getZip())
.phone(entity.getPhone())
.version(entity.getVersion())
.build();
}
private static TenantFields toFields(Tenant entity) {
return TenantFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getTitle())
.additionalInfo(getText(entity.getAdditionalInfo()))
.email(entity.getEmail())
.country(entity.getCountry())
.state(entity.getState())
.city(entity.getCity())
.address(entity.getAddress())
.address2(entity.getAddress2())
.zip(entity.getZip())
.phone(entity.getPhone())
.region(entity.getRegion())
.version(entity.getVersion())
.build();
}
private static TenantProfileFields toFields(TenantProfile tenantProfile) {
return TenantProfileFields.builder()
.id(tenantProfile.getUuidId())
.createdTime(tenantProfile.getCreatedTime())
.name(tenantProfile.getName())
.isDefault(tenantProfile.isDefault())
.build();
}
private static DeviceFields toFields(Device entity) {
return DeviceFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.customerId(getCustomerId(entity.getCustomerId()))
.name(entity.getName())
.type(entity.getType())
.deviceProfileId(entity.getDeviceProfileId().getId())
.label(entity.getLabel())
.additionalInfo(getText(entity.getAdditionalInfo()))
.version(entity.getVersion())
.build();
}
private static AssetFields toFields(Asset entity) {
return AssetFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.customerId(getCustomerId(entity.getCustomerId()))
.name(entity.getName())
.type(entity.getType())
.assetProfileId(entity.getAssetProfileId().getId())
.label(entity.getLabel())
.additionalInfo(getText(entity.getAdditionalInfo()))
.version(entity.getVersion())
.build();
}
private static EdgeFields toFields(Edge entity) {
return EdgeFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.customerId(getCustomerId(entity.getCustomerId()))
.name(entity.getName())
.type(entity.getType())
.label(entity.getLabel())
.additionalInfo(getText(entity.getAdditionalInfo()))
.version(entity.getVersion())
.build();
}
private static EntityViewFields toFields(EntityView entity) {
return EntityViewFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.customerId(getCustomerId(entity.getCustomerId()))
.name(entity.getName())
.type(entity.getType())
.additionalInfo(getText(entity.getAdditionalInfo()))
.version(entity.getVersion())
.build();
}
private static UserFields toFields(User entity) {
return UserFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.customerId(getCustomerId(entity.getCustomerId()))
.firstName(entity.getFirstName())
.lastName(entity.getLastName())
.email(entity.getEmail())
.phone(entity.getPhone())
.additionalInfo(getText(entity.getAdditionalInfo()))
.version(entity.getVersion())
.build();
}
private static DashboardFields toFields(Dashboard entity) {
return DashboardFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getTitle())
.version(entity.getVersion())
.build();
}
private static RuleChainFields toFields(RuleChain entity) {
return RuleChainFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getName())
.additionalInfo(getText(entity.getAdditionalInfo()))
.version(entity.getVersion())
.build();
}
private static RuleNodeFields toFields(RuleNode entity) {
return RuleNodeFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getName())
.additionalInfo(getText(entity.getAdditionalInfo()))
.build();
}
private static WidgetTypeFields toFields(WidgetType entity) {
return WidgetTypeFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getName())
.version(entity.getVersion())
.build();
}
private static WidgetsBundleFields toFields(WidgetsBundle entity) {
return WidgetsBundleFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getName())
.version(entity.getVersion())
.build();
}
private static AssetProfileFields toFields(AssetProfile entity) {
return AssetProfileFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getName())
.isDefault(entity.isDefault())
.version(entity.getVersion())
.build();
}
private static DeviceProfileFields toFields(DeviceProfile entity) {
return DeviceProfileFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.name(entity.getName())
.type(DeviceProfileType.DEFAULT.name())
.isDefault(entity.isDefault())
.version(entity.getVersion())
.build();
}
private static QueueStatsFields toFields(QueueStats entity) {
return QueueStatsFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.queueName(entity.getQueueName())
.serviceId(entity.getServiceId())
.build();
}
private static ApiUsageStateFields toFields(ApiUsageState entity) {
return ApiUsageStateFields.builder()
.id(entity.getUuidId())
.createdTime(entity.getCreatedTime())
.customerId(entity.getEntityId().getEntityType() == EntityType.CUSTOMER ? entity.getEntityId().getId() : null)
.entityId(entity.getEntityId())
.transportState(entity.getTransportState())
.dbStorageState(entity.getDbStorageState())
.reExecState(entity.getReExecState())
.jsExecState(entity.getJsExecState())
.tbelExecState(entity.getTbelExecState())
.emailExecState(entity.getEmailExecState())
.smsExecState(entity.getSmsExecState())
.alarmExecState(entity.getAlarmExecState())
.version(entity.getVersion())
.build();
}
public static String getText(JsonNode node) {
return node != null ? node.asText() : "";
}
private static UUID getCustomerId(CustomerId customerId) {
return (customerId != null && !customerId.getId().equals(CustomerId.NULL_UUID)) ? customerId.getId() : null;
}
}

29
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java

@ -0,0 +1,29 @@
/**
* 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.common.data.edqs.fields;
import lombok.NoArgsConstructor;
import java.util.UUID;
@NoArgsConstructor
public class GenericFields extends AbstractEntityFields {
public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version) {
super(id, createdTime, tenantId, name, version);
}
}

26
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/ProfileAwareFields.java

@ -0,0 +1,26 @@
/**
* 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.common.data.edqs.fields;
import java.util.UUID;
public interface ProfileAwareFields extends EntityFields {
String getProfileName();
UUID getProfileId();
}

42
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/QueueStatsFields.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.common.data.edqs.fields;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class QueueStatsFields extends AbstractEntityFields {
private String queueName;
private String serviceId;
@Override
public String getName() {
return queueName + '_' + serviceId;
}
public QueueStatsFields(UUID id, long createdTime, UUID tenantId, String queueName, String serviceId) {
super(id, createdTime, tenantId);
this.queueName = queueName;
this.serviceId = serviceId;
}
}

38
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleChainFields.java

@ -0,0 +1,38 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class RuleChainFields extends AbstractEntityFields {
private String additionalInfo;
public RuleChainFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo) {
super(id, createdTime, tenantId, name, version);
this.additionalInfo = getText(additionalInfo);
}
}

43
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/RuleNodeFields.java

@ -0,0 +1,43 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class RuleNodeFields implements EntityFields {
private UUID id;
private long createdTime;
private String name;
private String additionalInfo;
public RuleNodeFields(UUID id, long createdTime, String name, JsonNode additionalInfo) {
this.id = id;
this.createdTime = createdTime;
this.name = name;
this.additionalInfo = getText(additionalInfo);
}
}

63
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java

@ -0,0 +1,63 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class TenantFields extends AbstractEntityFields {
private String additionalInfo;
private String country;
private String state;
private String city;
private String address;
private String address2;
private String zip;
private String phone;
private String email;
private String region;
public TenantFields(UUID id, long createdTime, String name, Long version,
JsonNode additionalInfo, String country, String state, String city, String address,
String address2, String zip, String phone, String email, String region) {
super(id, createdTime, name, version);
this.additionalInfo = getText(additionalInfo);
this.country = country;
this.state = state;
this.city = city;
this.address = address;
this.address2 = address2;
this.zip = zip;
this.phone = phone;
this.email = email;
this.region = region;
}
@Override
public UUID getTenantId() {
return getId();
}
}

36
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantProfileFields.java

@ -0,0 +1,36 @@
/**
* 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.common.data.edqs.fields;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class TenantProfileFields extends AbstractEntityFields {
private boolean isDefault;
public TenantProfileFields(UUID id, long createdTime, String name, boolean isDefault) {
super(id, createdTime, TenantId.SYS_TENANT_ID.getId(), null, name, 0L);
this.isDefault = isDefault;
}
}

48
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java

@ -0,0 +1,48 @@
/**
* 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.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
public class UserFields extends AbstractEntityFields {
private String firstName;
private String lastName;
private String email;
private String phone;
private String additionalInfo;
public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId,
Long version, String firstName, String lastName, String email,
String phone, JsonNode additionalInfo) {
super(id, createdTime, tenantId, customerId, version);
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.additionalInfo = getText(additionalInfo);
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetTypeFields.java

@ -0,0 +1,30 @@
/**
* 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.common.data.edqs.fields;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@NoArgsConstructor
@SuperBuilder
public class WidgetTypeFields extends AbstractEntityFields {
public WidgetTypeFields(UUID id, long createdTime, UUID tenantId, String name, Long version) {
super(id, createdTime, tenantId, name, version);
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/WidgetsBundleFields.java

@ -0,0 +1,30 @@
/**
* 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.common.data.edqs.fields;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@NoArgsConstructor
@SuperBuilder
public class WidgetsBundleFields extends AbstractEntityFields {
public WidgetsBundleFields(UUID id, long createdTime, UUID tenantId, String name, Long version) {
super(id, createdTime, tenantId, name, version);
}
}

34
common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsRequest.java

@ -0,0 +1,34 @@
/**
* 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.common.data.edqs.query;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EdqsRequest {
private EntityDataQuery entityDataQuery;
private EntityCountQuery entityCountQuery;
}

35
common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/EdqsResponse.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.common.data.edqs.query;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class EdqsResponse {
private PageData<EntityData> entityDataQueryResult;
private Long entityCountQueryResult;
private String error;
}

39
common/data/src/main/java/org/thingsboard/server/common/data/edqs/query/QueryResult.java

@ -0,0 +1,39 @@
/**
* 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.common.data.edqs.query;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import java.util.Collections;
import java.util.Map;
@Data
@RequiredArgsConstructor
public class QueryResult {
private final EntityId entityId;
private final Map<EntityKeyType, Map<String, TsValue>> latest;
public EntityData toOldEntityData() {
return new EntityData(entityId, latest, Collections.emptyMap(), Collections.emptyMap());
}
}

6
common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java

@ -15,11 +15,15 @@
*/ */
package org.thingsboard.server.common.data.id; package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID; import java.util.UUID;
public class UserAuthSettingsId extends UUIDBased { public class UserAuthSettingsId extends UUIDBased {
public UserAuthSettingsId(UUID id) { @JsonCreator
public UserAuthSettingsId(@JsonProperty("id") UUID id) {
super(id); super(id);
} }

17
dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java → common/data/src/main/java/org/thingsboard/server/common/data/permission/QueryContext.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.dao.sql.query; package org.thingsboard.server.common.data.permission;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@ -21,8 +21,12 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@AllArgsConstructor @AllArgsConstructor
public class QuerySecurityContext { public class QueryContext {
@Getter @Getter
private final TenantId tenantId; private final TenantId tenantId;
@ -33,7 +37,14 @@ public class QuerySecurityContext {
@Getter @Getter
private final boolean ignorePermissionCheck; private final boolean ignorePermissionCheck;
public QuerySecurityContext(TenantId tenantId, CustomerId customerId, EntityType entityType) { @Getter
private final Map<UUID, UUID> relatedParentIdMap = new HashMap<>();
public QueryContext(TenantId tenantId, CustomerId customerId, EntityType entityType) {
this(tenantId, customerId, entityType, false); this(tenantId, customerId, entityType, false);
} }
public boolean isTenantUser() {
return customerId == null || customerId.isNullUid();
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.common.data.query; package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@ -26,7 +25,6 @@ import java.io.Serializable;
@RequiredArgsConstructor @RequiredArgsConstructor
public class DynamicValue<T> implements Serializable { public class DynamicValue<T> implements Serializable {
@JsonIgnore
private T resolvedValue; private T resolvedValue;
private final DynamicValueSourceType sourceType; private final DynamicValueSourceType sourceType;

2
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.query; package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
@ -24,6 +25,7 @@ import java.util.List;
@Schema @Schema
@ToString @ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class EntityCountQuery { public class EntityCountQuery {
@Getter @Getter

15
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java

@ -16,20 +16,22 @@
package org.thingsboard.server.common.data.query; package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map; import java.util.Map;
@Data @Data
@RequiredArgsConstructor @AllArgsConstructor
@NoArgsConstructor
public class EntityData { public class EntityData {
private final EntityId entityId; private EntityId entityId;
private final Map<EntityKeyType, Map<String, TsValue>> latest; private Map<EntityKeyType, Map<String, TsValue>> latest;
private final Map<String, TsValue[]> timeseries; private Map<String, TsValue[]> timeseries;
private final Map<Integer, ComparisonTsValue> aggLatest; private Map<Integer, ComparisonTsValue> aggLatest;
public EntityData(EntityId entityId, Map<EntityKeyType, Map<String, TsValue>> latest, Map<String, TsValue[]> timeseries) { public EntityData(EntityId entityId, Map<EntityKeyType, Map<String, TsValue>> latest, Map<String, TsValue[]> timeseries) {
this(entityId, latest, timeseries, null); this(entityId, latest, timeseries, null);
@ -44,4 +46,5 @@ public class EntityData {
aggLatest.clear(); aggLatest.clear();
} }
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save