Browse Source

Merge remote-tracking branch 'upstream/rc' into AD/bug-fix/b-r-elbow-con-position

pull/13229/head
Artem Dzhereleiko 2 years ago
parent
commit
ac1913791c
  1. 4
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 2
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  3. 1
      application/src/main/data/json/system/widget_types/gateway_status.json
  4. 2090
      application/src/main/data/resources/dashboards/gateways_dashboard.json
  5. 14
      application/src/main/data/resources/js_modules/gateway-management-extension.js
  6. 14
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  7. 2
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  8. 8
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
  9. 135
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  10. 8
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  12. 67
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java
  13. 95
      application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java
  14. 50
      application/src/main/java/org/thingsboard/server/service/cf/cache/TenantEntityProfileCache.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  18. 15
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  19. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  20. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  21. 56
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  22. 2
      application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java
  23. 10
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  25. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  26. 7
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  27. 13
      application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java
  28. 14
      application/src/main/resources/thingsboard.yml
  29. 2
      application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java
  30. 23
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  31. 22
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbEdgeQueueAdmin.java
  32. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  33. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  34. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  35. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  36. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java
  37. 6
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java
  38. 10
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java
  39. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  40. 5
      common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java
  41. 9
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java
  42. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java
  43. 5
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java
  44. 89
      common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java
  45. 39
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java
  46. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
  47. 33
      common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldCacheInitMsg.java
  48. 26
      common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitProfileEntityMsg.java
  49. 6
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java
  50. 2
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java
  51. 2
      common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java
  52. 36
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
  53. 3
      common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java
  54. 30
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
  55. 11
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
  56. 19
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java
  57. 9
      common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java
  58. 2
      common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCalculatedFieldSettings.java
  59. 6
      common/queue/src/main/java/org/thingsboard/server/queue/util/AfterStartUp.java
  60. 15
      common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java
  61. 16
      common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java
  62. 12
      common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java
  63. 8
      common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java
  64. 6
      common/util/src/main/java/org/thingsboard/common/util/TbBytePool.java
  65. 6
      common/util/src/main/java/org/thingsboard/common/util/TbStringPool.java
  66. 2
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
  67. 8
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  68. 16
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  69. 2
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java
  70. 4
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java
  71. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  72. 8
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  73. 2
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java
  74. 7
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  75. 19
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  76. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
  77. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java
  78. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java
  79. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  80. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java
  81. 17
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java
  82. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java
  83. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
  84. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/device/NativeProfileEntityRepository.java
  85. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java
  86. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java
  87. 2
      docker/compose-utils.sh
  88. 6
      docker/docker-install-tb.sh
  89. 6
      docker/docker-update-service.sh
  90. 9
      docker/docker-upgrade-tb.sh
  91. 1375
      docker/monitoring/grafana/provisioning/dashboards/edqs.json
  92. 161
      docker/monitoring/grafana/provisioning/dashboards/edqs_entities.json
  93. 4
      edqs/src/main/resources/edqs.yml
  94. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java
  95. 127
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/ProfileStateTest.java
  96. 22
      ui-ngx/src/app/core/utils.ts
  97. 4
      ui-ngx/src/app/modules/common/modules-map.ts
  98. 27
      ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.ts
  99. 1
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  100. 2
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

4
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/gateway_general_configuration.json

@ -16,7 +16,7 @@
],
"templateHtml": "<tb-gateway-configuration [defaultTab]=\"defaultTab\" [device]=\"entityId\" *ngIf=\"entityId\"></tb-gateway-configuration>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n self.ctx.$scope.defaultTab = self.ctx.stateController\n .getStateParams()?.defaultTab;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}", "settingsSchema": "{}",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n self.ctx.$scope.defaultTab = self.ctx.stateController?.getStateParams()?.defaultTab;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway configuration\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
},

1
application/src/main/data/json/system/widget_types/gateway_status.json

@ -19,7 +19,6 @@
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayStatus?.onDataUpdated();\n};",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-gateway-status",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}"
},
"tags": [

2090
application/src/main/data/resources/dashboards/gateways_dashboard.json

File diff suppressed because it is too large

14
application/src/main/data/resources/js_modules/gateway-management-extension.js

File diff suppressed because one or more lines are too long

14
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -30,9 +30,9 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -106,11 +106,11 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.CalculatedFieldQueueService;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
@ -460,6 +460,11 @@ public class ActorSystemContext {
@Getter
private DebugModeRateLimitsConfig debugModeRateLimitsConfig;
@Lazy
@Autowired(required = false)
@Getter
private TbQueueCalculatedFieldSettings calculatedFieldSettings;
/**
* The following Service will be null if we operate in tb-core mode
*/
@ -546,11 +551,6 @@ public class ActorSystemContext {
@Getter
private CalculatedFieldQueueService calculatedFieldQueueService;
@Lazy
@Autowired(required = false)
@Getter
private CalculatedFieldEntityProfileCache calculatedFieldEntityProfileCache;
@Value("${actors.session.max_concurrent_sessions_per_device:1}")
@Getter
private int maxConcurrentSessionsPerDevice;

2
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -113,6 +113,8 @@ public class AppActor extends ContextAwareActor {
case SESSION_TIMEOUT_MSG:
ctx.broadcastToChildrenByType(msg, EntityType.TENANT);
break;
case CF_CACHE_INIT_MSG:
case CF_INIT_PROFILE_ENTITY_MSG:
case CF_INIT_MSG:
case CF_LINK_INIT_MSG:
case CF_STATE_RESTORE_MSG:

8
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java

@ -22,8 +22,10 @@ import org.thingsboard.server.actors.TbActorException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
@ -65,6 +67,12 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_PARTITIONS_CHANGE_MSG:
processor.onPartitionChange((CalculatedFieldPartitionChangeMsg) msg);
break;
case CF_CACHE_INIT_MSG:
processor.onCacheInitMsg((CalculatedFieldCacheInitMsg) msg);
break;
case CF_INIT_PROFILE_ENTITY_MSG:
processor.onProfileEntityMsg((CalculatedFieldInitProfileEntityMsg) msg);
break;
case CF_INIT_MSG:
processor.onFieldInitMsg((CalculatedFieldInitMsg) msg);
break;

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

@ -24,6 +24,7 @@ import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AssetId;
@ -31,17 +32,23 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache;
import org.thingsboard.server.service.cf.cache.TenantEntityProfileCache;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -68,22 +75,28 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private final CalculatedFieldProcessingService cfExecService;
private final CalculatedFieldStateService cfStateService;
private final CalculatedFieldEntityProfileCache cfEntityCache;
private final CalculatedFieldService cfDaoService;
private final DeviceService deviceService;
private final AssetService assetService;
private final TbAssetProfileCache assetProfileCache;
private final TbDeviceProfileCache deviceProfileCache;
private final TenantEntityProfileCache entityProfileCache;
private final TbQueueCalculatedFieldSettings cfSettings;
protected final TenantId tenantId;
protected TbActorCtx ctx;
CalculatedFieldManagerMessageProcessor(ActorSystemContext systemContext, TenantId tenantId) {
super(systemContext);
this.cfEntityCache = systemContext.getCalculatedFieldEntityProfileCache();
this.cfExecService = systemContext.getCalculatedFieldProcessingService();
this.cfStateService = systemContext.getCalculatedFieldStateService();
this.cfDaoService = systemContext.getCalculatedFieldService();
this.deviceService = systemContext.getDeviceService();
this.assetService = systemContext.getAssetService();
this.assetProfileCache = systemContext.getAssetProfileCache();
this.deviceProfileCache = systemContext.getDeviceProfileCache();
this.entityProfileCache = new TenantEntityProfileCache();
this.cfSettings = systemContext.getCalculatedFieldSettings();
this.tenantId = tenantId;
}
@ -100,6 +113,19 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
ctx.stop(ctx.getSelf());
}
public void onCacheInitMsg(CalculatedFieldCacheInitMsg msg) {
log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId());
initEntityProfileCache();
initCalculatedFields();
msg.getCallback().onSuccess();
}
public void onProfileEntityMsg(CalculatedFieldInitProfileEntityMsg msg) {
log.debug("[{}] Processing profile entity message.", msg.getTenantId().getId());
entityProfileCache.add(msg.getProfileEntityId(), msg.getEntityId());
msg.getCallback().onSuccess();
}
public void onFieldInitMsg(CalculatedFieldInitMsg msg) throws CalculatedFieldException {
log.debug("[{}] Processing CF init message.", msg.getCf().getId());
var cf = msg.getCf();
@ -180,16 +206,35 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
break;
}
case DEVICE_PROFILE:
case ASSET_PROFILE: {
switch (event) {
case DELETED:
onProfileDeleted(msg.getData(), msg.getCallback());
break;
default:
msg.getCallback().onSuccess();
break;
}
break;
}
default: {
msg.getCallback().onSuccess();
}
}
}
private void onProfileDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
entityProfileCache.removeProfileId(msg.getEntityId());
callback.onSuccess();
}
private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) {
EntityId entityId = msg.getEntityId();
EntityId profileId = getProfileId(tenantId, entityId);
cfEntityCache.add(tenantId, profileId, entityId);
if (profileId != null) {
entityProfileCache.add(profileId, entityId);
}
if (!isMyPartition(entityId, callback)) {
return;
}
@ -207,7 +252,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private void onEntityUpdated(ComponentLifecycleMsg msg, TbCallback callback) {
if (msg.getOldProfileId() != null && !msg.getOldProfileId().equals(msg.getProfileId())) {
cfEntityCache.update(tenantId, msg.getOldProfileId(), msg.getProfileId(), msg.getEntityId());
entityProfileCache.update(msg.getOldProfileId(), msg.getProfileId(), msg.getEntityId());
if (!isMyPartition(msg.getEntityId(), callback)) {
return;
}
@ -226,7 +271,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
cfEntityCache.evict(tenantId, msg.getEntityId());
entityProfileCache.removeEntityId(msg.getEntityId());
if (isMyPartition(msg.getEntityId(), callback)) {
log.debug("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId());
getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback));
@ -322,11 +367,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
EntityId entityId = cfCtx.getEntityId();
EntityType entityType = cfCtx.getEntityId().getEntityType();
if (isProfileEntity(entityType)) {
var entityIds = cfEntityCache.getMyEntityIdsByProfileId(tenantId, entityId);
var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId);
if (!entityIds.isEmpty()) {
//TODO: no need to do this if we cache all created actors and know which one belong to us;
var multiCallback = new MultipleTbCallback(entityIds.size(), callback);
entityIds.forEach(id -> deleteCfForEntity(id, cfId, multiCallback));
entityIds.forEach(id -> {
if (isMyPartition(id, multiCallback)) {
deleteCfForEntity(id, cfId, multiCallback);
}
});
} else {
callback.onSuccess();
}
@ -366,10 +415,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
EntityId sourceEntityId = msg.getEntityId();
log.debug("Received linked telemetry msg from entity [{}]", sourceEntityId);
var proto = msg.getProto();
var callback = msg.getCallback();
var linksList = proto.getLinksList();
if (linksList.isEmpty()) {
log.debug("[{}] No CF links to process new telemetry.", msg.getTenantId());
msg.getCallback().onSuccess();
callback.onSuccess();
}
for (var linkProto : linksList) {
var link = fromProto(linkProto);
@ -378,21 +428,23 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var cf = calculatedFields.get(link.cfId());
if (EntityType.DEVICE_PROFILE.equals(targetEntityType) || EntityType.ASSET_PROFILE.equals(targetEntityType)) {
// iterate over all entities that belong to profile and push the message for corresponding CF
var entityIds = cfEntityCache.getMyEntityIdsByProfileId(tenantId, targetEntityId);
var entityIds = entityProfileCache.getEntityIdsByProfileId(targetEntityId);
if (!entityIds.isEmpty()) {
MultipleTbCallback callback = new MultipleTbCallback(entityIds.size(), msg.getCallback());
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback);
MultipleTbCallback multipleCallback = new MultipleTbCallback(entityIds.size(), callback);
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, multipleCallback);
entityIds.forEach(entityId -> {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(newMsg);
if (isMyPartition(entityId, multipleCallback)) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(newMsg);
}
});
} else {
msg.getCallback().onSuccess();
callback.onSuccess();
}
} else {
if (isMyPartition(targetEntityId, msg.getCallback())) {
if (isMyPartition(targetEntityId, callback)) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId);
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, msg.getCallback());
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback);
getOrCreateActor(targetEntityId).tell(newMsg);
}
}
@ -438,10 +490,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
EntityId entityId = cfCtx.getEntityId();
EntityType entityType = cfCtx.getEntityId().getEntityType();
if (isProfileEntity(entityType)) {
var entityIds = cfEntityCache.getMyEntityIdsByProfileId(tenantId, entityId);
var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId);
if (!entityIds.isEmpty()) {
var multiCallback = new MultipleTbCallback(entityIds.size(), callback);
entityIds.forEach(id -> initCfForEntity(id, cfCtx, forceStateReinit, multiCallback));
entityIds.forEach(id -> {
if (isMyPartition(id, multiCallback)) {
initCfForEntity(id, cfCtx, forceStateReinit, multiCallback);
}
});
} else {
callback.onSuccess();
}
@ -504,4 +560,45 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) {
ctx.broadcastToChildren(msg, true);
}
public void initCalculatedFields() {
PageDataIterable<CalculatedField> cfs = new PageDataIterable<>(pageLink -> cfDaoService.findCalculatedFieldsByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
cfs.forEach(cf -> {
log.trace("Processing calculated field record: {}", cf);
try {
onFieldInitMsg(new CalculatedFieldInitMsg(cf.getTenantId(), cf));
} catch (CalculatedFieldException e) {
log.error("Failed to process calculated field record: {}", cf, e);
}
});
calculatedFields.values().forEach(cf -> {
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf);
});
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
cfls.forEach(link -> {
onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link));
});
}
private void initEntityProfileCache() {
PageDataIterable<ProfileEntityIdInfo> deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
for (ProfileEntityIdInfo idInfo : deviceIdInfos) {
log.trace("Processing device record: {}", idInfo);
try {
entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId());
} catch (Exception e) {
log.error("Failed to process device record: {}", idInfo, e);
}
}
PageDataIterable<ProfileEntityIdInfo> assetIdInfos = new PageDataIterable<>(pageLink -> assetService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
for (ProfileEntityIdInfo idInfo : assetIdInfos) {
log.trace("Processing asset record: {}", idInfo);
try {
entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId());
} catch (Exception e) {
log.error("Failed to process asset record: {}", idInfo, e);
}
}
}
}

8
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
@ -99,6 +100,7 @@ public class TenantActor extends RuleChainManagerActor {
() -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME,
() -> new CalculatedFieldManagerActorCreator(systemContext, tenantId),
() -> true);
cfActor.tellWithHighPriority(new CalculatedFieldCacheInitMsg(tenantId));
} catch (Exception e) {
log.info("[{}] Failed to init CF Actor.", tenantId, e);
}
@ -129,6 +131,7 @@ public class TenantActor extends RuleChainManagerActor {
log.info("[{}] Stopping tenant actor.", tenantId);
if (cfActor != null) {
ctx.stop(cfActor.getActorId());
cfActor = null;
}
}
@ -176,6 +179,8 @@ public class TenantActor extends RuleChainManagerActor {
case RULE_CHAIN_TO_RULE_CHAIN_MSG:
onRuleChainMsg((RuleChainAwareMsg) msg);
break;
case CF_CACHE_INIT_MSG:
case CF_INIT_PROFILE_ENTITY_MSG:
case CF_INIT_MSG:
case CF_LINK_INIT_MSG:
case CF_STATE_RESTORE_MSG:
@ -199,6 +204,7 @@ public class TenantActor extends RuleChainManagerActor {
} else {
log.debug("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg);
}
msg.getCallback().onSuccess();
return;
}
if (priority) {
@ -274,6 +280,7 @@ public class TenantActor extends RuleChainManagerActor {
() -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME,
() -> new CalculatedFieldManagerActorCreator(systemContext, tenantId),
() -> true);
cfActor.tellWithHighPriority(new CalculatedFieldCacheInitMsg(tenantId));
} catch (Exception e) {
log.info("[{}] Failed to init CF Actor.", tenantId, e);
}
@ -285,6 +292,7 @@ public class TenantActor extends RuleChainManagerActor {
} else {
if (cfActor != null) {
ctx.stop(cfActor.getActorId());
cfActor = null;
}
if (ruleChainsInitialized) {
log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId);

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

@ -64,7 +64,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
private final ConcurrentMap<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new ConcurrentHashMap<>();
private final ConcurrentMap<CalculatedFieldId, CalculatedFieldCtx> calculatedFieldsCtx = new ConcurrentHashMap<>();
@Value("${calculatedField.initFetchPackSize:50000}")
@Value("${queue.calculated_fields.init_fetch_pack_size:50000}")
@Getter
private int initFetchPackSize;

67
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java

@ -1,67 +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.cf;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.cf.cache.CalculatedFieldEntityProfileCache;
@Slf4j
@Service
@TbRuleEngineComponent
@RequiredArgsConstructor
public class DefaultCalculatedFieldInitService implements CalculatedFieldInitService {
private final CalculatedFieldEntityProfileCache entityProfileCache;
private final AssetService assetService;
private final DeviceService deviceService;
@Value("${calculated_fields.init_fetch_pack_size:50000}")
@Getter
private int initFetchPackSize;
@AfterStartUp(order = AfterStartUp.CF_READ_PROFILE_ENTITIES_SERVICE)
public void initCalculatedFieldDefinitions() {
PageDataIterable<ProfileEntityIdInfo> deviceIdInfos = new PageDataIterable<>(deviceService::findProfileEntityIdInfos, initFetchPackSize);
for (ProfileEntityIdInfo idInfo : deviceIdInfos) {
log.trace("Processing device record: {}", idInfo);
try {
entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId());
} catch (Exception e) {
log.error("Failed to process device record: {}", idInfo, e);
}
}
PageDataIterable<ProfileEntityIdInfo> assetIdInfos = new PageDataIterable<>(assetService::findProfileEntityIdInfos, initFetchPackSize);
for (ProfileEntityIdInfo idInfo : assetIdInfos) {
log.trace("Processing asset record: {}", idInfo);
try {
entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId());
} catch (Exception e) {
log.error("Failed to process asset record: {}", idInfo, e);
}
}
}
}

95
application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java

@ -1,95 +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.cf.cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
@TbRuleEngineComponent
@Service
@Slf4j
@RequiredArgsConstructor
//TODO ashvayka: remove and use TenantEntityProfileCache in each CalculatedFieldManagerMessageProcessor;
public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEventListener<PartitionChangeEvent> implements CalculatedFieldEntityProfileCache {
private static final Integer UNKNOWN = 0;
private final ConcurrentMap<TenantId, TenantEntityProfileCache> tenantCache = new ConcurrentHashMap<>();
private final PartitionService partitionService;
private volatile List<Integer> myPartitions = Collections.emptyList();
@Override
protected void onTbApplicationEvent(PartitionChangeEvent event) {
myPartitions = event.getCfPartitions().stream()
.filter(TopicPartitionInfo::isMyPartition)
.map(tpi -> tpi.getPartition().orElse(UNKNOWN)).collect(Collectors.toList());
//Naive approach that need to be improved.
tenantCache.values().forEach(cache -> cache.setMyPartitions(myPartitions));
}
@Override
public void add(TenantId tenantId, EntityId profileId, EntityId entityId) {
var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
var partition = tpi.getPartition().orElse(UNKNOWN);
tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache())
.add(profileId, entityId, partition, tpi.isMyPartition());
}
@Override
public void update(TenantId tenantId, EntityId oldProfileId, EntityId newProfileId, EntityId entityId) {
var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
var partition = tpi.getPartition().orElse(UNKNOWN);
var cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache());
//TODO: make this method atomic;
cache.remove(oldProfileId, entityId);
cache.add(newProfileId, entityId, partition, tpi.isMyPartition());
}
@Override
public void evict(TenantId tenantId, EntityId entityId) {
var cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache());
cache.removeEntityId(entityId);
}
@Override
public Collection<EntityId> getMyEntityIdsByProfileId(TenantId tenantId, EntityId profileId) {
return tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()).getMyEntityIdsByProfileId(profileId);
}
@Override
public int getEntityIdPartition(TenantId tenantId, EntityId entityId) {
var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
return tpi.getPartition().orElse(UNKNOWN);
}
}

50
application/src/main/java/org/thingsboard/server/service/cf/cache/TenantEntityProfileCache.java

@ -32,31 +32,13 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TenantEntityProfileCache {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<Integer, Map<EntityId, Set<EntityId>>> allEntities = new HashMap<>();
private final Map<EntityId, Set<EntityId>> myEntities = new HashMap<>();
public void setMyPartitions(List<Integer> myPartitions) {
lock.writeLock().lock();
try {
myEntities.clear();
myPartitions.forEach(partitionId -> {
var map = allEntities.get(partitionId);
if (map != null) {
map.forEach((profileId, entityIds) -> myEntities.computeIfAbsent(profileId, k -> new HashSet<>()).addAll(entityIds));
}
});
} finally {
lock.writeLock().unlock();
}
}
private final Map<EntityId, Set<EntityId>> allEntities = new HashMap<>();
public void removeProfileId(EntityId profileId) {
lock.writeLock().lock();
try {
// Remove from allEntities
allEntities.values().forEach(map -> map.remove(profileId));
// Remove from myEntities
myEntities.remove(profileId);
allEntities.remove(profileId);
} finally {
lock.writeLock().unlock();
}
@ -66,9 +48,7 @@ public class TenantEntityProfileCache {
lock.writeLock().lock();
try {
// Remove from allEntities
allEntities.values().forEach(map -> map.values().forEach(set -> set.remove(entityId)));
// Remove from myEntities
myEntities.values().forEach(set -> set.remove(entityId));
allEntities.values().forEach(set -> set.remove(entityId));
} finally {
lock.writeLock().unlock();
}
@ -78,33 +58,33 @@ public class TenantEntityProfileCache {
lock.writeLock().lock();
try {
// Remove from allEntities
allEntities.values().forEach(map -> removeSafely(map, profileId, entityId));
// Remove from myEntities
removeSafely(myEntities, profileId, entityId);
removeSafely(allEntities, profileId, entityId);
} finally {
lock.writeLock().unlock();
}
}
public void add(EntityId profileId, EntityId entityId, Integer partition, boolean mine) {
public void add(EntityId profileId, EntityId entityId) {
lock.writeLock().lock();
try {
if(EntityType.DEVICE.equals(profileId.getEntityType())){
throw new RuntimeException("WTF?");
}
if (mine) {
myEntities.computeIfAbsent(profileId, k -> new HashSet<>()).add(entityId);
if (EntityType.DEVICE.equals(profileId.getEntityType()) || EntityType.ASSET.equals(profileId.getEntityType())) {
throw new RuntimeException("Entity type '" + profileId.getEntityType() + "' is not a profileId.");
}
allEntities.computeIfAbsent(partition, k -> new HashMap<>()).computeIfAbsent(profileId, p -> new HashSet<>()).add(entityId);
allEntities.computeIfAbsent(profileId, k -> new HashSet<>()).add(entityId);
} finally {
lock.writeLock().unlock();
}
}
public Collection<EntityId> getMyEntityIdsByProfileId(EntityId profileId) {
public void update(EntityId oldProfileId, EntityId newProfileId, EntityId entityId) {
remove(oldProfileId, entityId);
add(newProfileId, entityId);
}
public Collection<EntityId> getEntityIdsByProfileId(EntityId profileId) {
lock.readLock().lock();
try {
var entities = myEntities.getOrDefault(profileId, Collections.emptySet());
var entities = allEntities.getOrDefault(profileId, Collections.emptySet());
List<EntityId> result = new ArrayList<>(entities.size());
result.addAll(entities);
return result;

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java

@ -91,7 +91,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
}
}
})
.consumerCreator((config, partitionId) -> queueFactory.createCalculatedFieldStateConsumer())
.consumerCreator((queueConfig, tpi) -> queueFactory.createCalculatedFieldStateConsumer())
.queueAdmin(queueFactory.getCalculatedFieldQueueAdmin())
.consumerExecutor(eventConsumer.getConsumerExecutor())
.scheduler(eventConsumer.getScheduler())

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java

@ -22,11 +22,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.common.state.DefaultQueueStateService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
import org.thingsboard.server.queue.common.state.DefaultQueueStateService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService;
import org.thingsboard.server.service.cf.CfRocksDb;

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

@ -167,6 +167,11 @@ public class EdgeEventSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(RelationActionEvent event) {
try {
TenantId tenantId = event.getTenantId();
if (ActionType.RELATION_DELETED.equals(event.getActionType()) && !tenantService.tenantExists(tenantId)) {
log.debug("[{}] Ignoring RelationActionEvent because tenant does not exist: {}", tenantId, event);
return;
}
EntityRelation relation = event.getRelation();
if (relation == null) {
log.trace("[{}] skipping RelationActionEvent event in case relation is null: {}", event.getTenantId(), event);

15
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -28,6 +28,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode;
import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode;
import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode;
import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode;
import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode;
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode;
import org.thingsboard.server.common.adaptor.JsonConverter;
@ -129,6 +130,11 @@ import java.util.UUID;
@Slf4j
public class EdgeMsgConstructorUtils {
public static final Map<EdgeVersion, Map<String, String>> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of(
EdgeVersion.V_3_9_0,
Map.of(
TbMsgTimeseriesNode.class.getName(), "processingSettings",
TbMsgAttributesNode.class.getName(), "processingSettings"
),
EdgeVersion.V_3_8_0,
Map.of(
TbMsgTimeseriesNode.class.getName(), "processingSettings",
@ -144,8 +150,17 @@ public class EdgeMsgConstructorUtils {
);
public static final Map<EdgeVersion, Set<String>> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of(
EdgeVersion.V_3_9_0,
Set.of(
TbCalculatedFieldsNode.class.getName()
),
EdgeVersion.V_3_8_0,
Set.of(
TbCalculatedFieldsNode.class.getName()
),
EdgeVersion.V_3_7_0,
Set.of(
TbCalculatedFieldsNode.class.getName(),
TbSendRestApiCallReplyNode.class.getName(),
TbAwsLambdaNode.class.getName()
)

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -528,7 +528,7 @@ public abstract class EdgeGrpcSession implements Closeable {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg);
} else {
log.error("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
DownlinkMsg downlinkMsg = sessionState.getPendingMsgsMap().get(msg.getDownlinkMsgId());
// if NOT timeseries or attributes failures - ack failed downlink
if (downlinkMsg.getEntityDataCount() == 0) {

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -141,6 +141,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic();
TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs());
kafkaAdmin.deleteTopic(topic);
kafkaAdmin.deleteConsumerGroup(topic);
}
}

56
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -95,28 +95,42 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
EdgeEventActionType action,
EntityId entityId,
JsonNode body) {
ListenableFuture<Optional<AttributeKvEntry>> future =
edgeCtx.getAttributesService().find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DefaultDeviceStateService.ACTIVITY_STATE);
return Futures.transformAsync(future, activeOpt -> {
if (activeOpt.isEmpty()) {
log.trace("Edge is not activated. Skipping event. tenantId [{}], edgeId [{}], type[{}], " +
"action [{}], entityId [{}], body [{}]",
tenantId, edgeId, type, action, entityId, body);
return Futures.immediateFuture(null);
}
if (activeOpt.get().getBooleanValue().isPresent() && activeOpt.get().getBooleanValue().get()) {
return doSaveEdgeEvent(tenantId, edgeId, type, action, entityId, body);
} else {
if (doSaveIfEdgeIsOffline(type, action)) {
return doSaveEdgeEvent(tenantId, edgeId, type, action, entityId, body);
} else {
log.trace("Edge is not active at the moment. Skipping event. tenantId [{}], edgeId [{}], type[{}], " +
return saveEdgeEvent(tenantId, edgeId, type, action, entityId, body, true);
}
protected ListenableFuture<Void> saveEdgeEvent(TenantId tenantId,
EdgeId edgeId,
EdgeEventType type,
EdgeEventActionType action,
EntityId entityId,
JsonNode body,
boolean doValidate) {
if (doValidate) {
ListenableFuture<Optional<AttributeKvEntry>> future =
edgeCtx.getAttributesService().find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DefaultDeviceStateService.ACTIVITY_STATE);
return Futures.transformAsync(future, activeOpt -> {
if (activeOpt.isEmpty()) {
log.trace("Edge is not activated. Skipping event. tenantId [{}], edgeId [{}], type[{}], " +
"action [{}], entityId [{}], body [{}]",
tenantId, edgeId, type, action, entityId, body);
return Futures.immediateFuture(null);
}
}
}, dbCallbackExecutorService);
if (activeOpt.get().getBooleanValue().isPresent() && activeOpt.get().getBooleanValue().get()) {
return doSaveEdgeEvent(tenantId, edgeId, type, action, entityId, body);
} else {
if (doSaveIfEdgeIsOffline(type, action)) {
return doSaveEdgeEvent(tenantId, edgeId, type, action, entityId, body);
} else {
log.trace("Edge is not active at the moment. Skipping event. tenantId [{}], edgeId [{}], type[{}], " +
"action [{}], entityId [{}], body [{}]",
tenantId, edgeId, type, action, entityId, body);
return Futures.immediateFuture(null);
}
}
}, dbCallbackExecutorService);
} else {
return doSaveEdgeEvent(tenantId, edgeId, type, action, entityId, body);
}
}
private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) {
@ -145,9 +159,9 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
JsonNode body, EdgeId sourceEdgeId) {
List<ListenableFuture<Void>> futures = new ArrayList<>();
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
PageDataIterable<TenantId> tenantIds = new PageDataIterable<>(link -> edgeCtx.getTenantService().findTenantsIds(link), 1024);
for (TenantId tenantId1 : tenantIds) {
futures.addAll(processActionForAllEdgesByTenantId(tenantId1, type, actionType, entityId, body, sourceEdgeId));
PageDataIterable<Edge> edges = new PageDataIterable<>(link -> edgeCtx.getEdgeService().findActiveEdges(link), 1024);
for (Edge edge : edges) {
futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), type, actionType, entityId, body, false));
}
} else {
futures = processActionForAllEdgesByTenantId(tenantId, type, actionType, entityId, null, sourceEdgeId);

2
application/src/main/java/org/thingsboard/server/service/housekeeper/stats/HousekeeperStatsService.java

@ -106,7 +106,7 @@ public class HousekeeperStatsService {
this.failedProcessingCounter = register("failedProcessing", statsFactory);
this.reprocessedCounter = register("reprocessed", statsFactory);
this.failedReprocessingCounter = register("failedReprocessing", statsFactory);
this.processingTimer = statsFactory.createTimer(StatsType.HOUSEKEEPER, "processingTime", "taskType", taskType.name());
this.processingTimer = statsFactory.createStatsTimer(StatsType.HOUSEKEEPER.getName(), "processingTime", "taskType", taskType.name());
}
private StatsCounter register(String statsName, StatsFactory statsFactory) {

10
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -99,12 +99,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
@Override
protected void onStartUp() {
var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME);
PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create()
var eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create()
.queueKey(queueKey)
.topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval)
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer())
.consumerCreator((queueConfig, tpi) -> queueFactory.createToCalculatedFieldMsgConsumer(tpi))
.queueAdmin(queueFactory.getCalculatedFieldQueueAdmin())
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
@ -127,7 +127,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
protected void onPartitionChangeEvent(PartitionChangeEvent event) {
try {
event.getNewPartitions().forEach((queueKey, partitions) -> {
if (queueKey.getQueueName().equals(DataConstants.CF_QUEUE_NAME)) {
if (DataConstants.CF_QUEUE_NAME.equals(queueKey.getQueueName())) {
stateService.restore(queueKey, partitions);
}
});
@ -175,9 +175,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
packSubmitFuture.cancel(true);
log.info("Timeout to process message: {}", pendingMsgHolder.getMsg());
}
if (log.isDebugEnabled()) {
ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
}
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process message: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
}
consumer.commit();

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
@ -116,6 +117,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@Slf4j
@TbCoreComponent
public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCoreNotificationMsg> implements TbCoreConsumerService {
@ -204,7 +206,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
.queueKey(new QueueKey(ServiceType.TB_CORE))
.config(QueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createToCoreMsgConsumer())
.consumerCreator((config, tpi) -> queueFactory.createToCoreMsgConsumer())
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)

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

@ -102,7 +102,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
.queueKey(new QueueKey(ServiceType.TB_CORE).withQueueName(DataConstants.EDGE_QUEUE_NAME))
.config(QueueConfig.of(consumerPerPartition, pollInterval))
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createEdgeMsgConsumer())
.consumerCreator((config, tpi) -> queueFactory.createEdgeMsgConsumer())
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)

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

@ -72,7 +72,12 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
ExecutorService consumerExecutor,
ScheduledExecutorService scheduler,
ExecutorService taskExecutor) {
super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor, null);
super(queueKey, null, null,
(queueConfig, tpi) -> {
Integer partitionId = tpi != null ? tpi.getPartition().orElse(-1) : null;
return ctx.getQueueFactory().createToRuleEngineMsgConsumer(queueConfig, partitionId);
},
consumerExecutor, scheduler, taskExecutor, null);
this.ctx = ctx;
this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory());
}

13
application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java

@ -113,7 +113,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
.ifPresentOrElse(lastConnectTime -> {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic();
if (kafkaAdmin.isTopicEmpty(topic)) {
kafkaAdmin.deleteTopic(topic);
deleteTopicAndConsumerGroup(topic);
log.info("[{}] Removed outdated topic {} for edge {} older than {}",
tenantId, topic, edgeId, Date.from(Instant.ofEpochMilli(currentTimeMillis - ttlMillis)));
}
@ -121,7 +121,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
Edge edge = edgeService.findEdgeById(tenantId, edgeId);
if (edge == null) {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic();
kafkaAdmin.deleteTopic(topic);
deleteTopicAndConsumerGroup(topic);
log.info("[{}] Removed topic {} for deleted edge {}", tenantId, topic, edgeId);
}
});
@ -132,12 +132,17 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
} else {
for (EdgeId edgeId : edgeIds) {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic();
kafkaAdmin.deleteTopic(topic);
deleteTopicAndConsumerGroup(topic);
}
log.info("[{}] Removed topics for not existing tenant and edges {}", tenantId, edgeIds);
}
}
private void deleteTopicAndConsumerGroup(String topic) {
kafkaAdmin.deleteTopic(topic);
kafkaAdmin.deleteConsumerGroup(topic);
}
private boolean isTopicExpired(long lastConnectTime, long ttlMillis, long currentTimeMillis) {
return lastConnectTime + ttlMillis < currentTimeMillis;
}
@ -146,7 +151,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
Map<TenantId, List<EdgeId>> tenantEdgeMap = new HashMap<>();
for (String topic : topics) {
try {
String remaining = topic.substring(prefix.length());
String remaining = topic.substring(prefix.length() + 1);
String[] parts = remaining.split("\\.");
TenantId tenantId = TenantId.fromUUID(UUID.fromString(parts[0]));
EdgeId edgeId = new EdgeId(UUID.fromString(parts[1]));

14
application/src/main/resources/thingsboard.yml

@ -1600,13 +1600,13 @@ queue:
# These notifications include RPC calls, lifecycle events, and new queue messages,
# requiring minimal latency and swift processing.
- key: max.poll.records
# Define the maximum number of records that can be polled from tb_edge.notifications.<SERVICE_ID> topics.
# Define the maximum number of records that can be polled from tb_edge.notifications.SERVICE_ID topics.
value: "${TB_QUEUE_KAFKA_EDGE_HP_EVENTS_MAX_POLL_RECORDS:10}"
tb_edge_event.notifications:
# Properties for consumers targeting downlinks meant for specific edge topics.
# Topic names are dynamically constructed using tenant and edge identifiers.
- key: max.poll.records
# Define the maximum number of records that can be polled from tb_edge_event.notifications.<TENANT_ID>.<EDGE_ID> topics.
# Define the maximum number of records that can be polled from tb_edge_event.notifications.TENANT_ID.EDGE_ID topics.
value: "${TB_QUEUE_KAFKA_EDGE_NOTIFICATIONS_MAX_POLL_RECORDS:10}"
tb_housekeeper:
# Consumer properties for Housekeeper tasks topic
@ -1773,7 +1773,7 @@ queue:
# EDQS responses topic
responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}"
# Poll interval for EDQS topics
poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}"
poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:25}"
# Maximum amount of pending requests to EDQS
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
# Maximum timeout for requests to EDQS
@ -1848,6 +1848,10 @@ queue:
pool_size: "${TB_QUEUE_CF_POOL_SIZE:8}"
# RocksDB path for storing CF states
rocks_db_path: "${TB_QUEUE_CF_ROCKS_DB_PATH:${user.home}/.rocksdb/cf_states}"
# The fetch size specifies how many rows will be fetched from the database per request for initial fetching
init_fetch_pack_size: "${TB_QUEUE_CF_FETCH_PACK_SIZE:50000}"
# The fetch size specifies how many rows will be fetched from the database per request for per-tenant fetching
init_tenant_fetch_pack_size: "${TB_QUEUE_CF_TENANT_FETCH_PACK_SIZE:1000}"
transport:
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}"
@ -1857,10 +1861,10 @@ queue:
# Topic name to notify edge service on entity updates, assignment, etc.
topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}"
# Topic prefix for high-priority edge notifications (rpc, lifecycle, new messages in queue) that require minimum latency and processing time.
# Each tb-core has its own topic: <PREFIX>.<SERVICE_ID>
# Each tb-core has its own topic: PREFIX.SERVICE_ID
notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}"
# Topic prefix for downlinks to be pushed to specific edge.
# Every edge has its own unique topic: <PREFIX>.<TENANT_ID>.<EDGE_ID>
# Every edge has its own unique topic: PREFIX.TENANT_ID.EDGE_ID
event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}"
# Amount of partitions used by Edge services
partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}"

2
application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java

@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.math.TbMathNode;
import org.thingsboard.rule.engine.metadata.CalculateDeltaNode;
import org.thingsboard.rule.engine.metadata.TbGetTelemetryNode;
import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode;
import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode;
import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode;
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -71,6 +72,7 @@ public class EdgeMsgConstructorUtilsTest {
new TbMsgTimeseriesNode(),
new TbSendRestApiCallReplyNode(),
new TbAwsLambdaNode(),
new TbCalculatedFieldsNode(),
new TbMathNode(),
new CalculateDeltaNode(),

23
application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

@ -1116,6 +1116,29 @@ public class EntityServiceTest extends AbstractControllerTest {
assertThat(deviceName).isEqualTo(devices.get(0).getName());
}
@Test
public void testFindCustomerBySingleEntityFilter() {
SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(customerId);
List<EntityKey> entityFields = List.of(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
);
EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null);
EntityDataQuery query = new EntityDataQuery(singleEntityFilter, pageLink, entityFields, null, null);
//find by tenant
PageData<EntityData> result = findByQueryAndCheck(query, 1);
String customerName = result.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
assertThat(customerName).isEqualTo(TEST_CUSTOMER_NAME);
// find by customer user
PageData<EntityData> customerResults = findByQueryAndCheck(customerId, query, 1);
customerName = customerResults.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue();
assertThat(customerName).isEqualTo(TEST_CUSTOMER_NAME);
}
@Test
public void testFindEntitiesByRelationEntityTypeFilter() {
Customer customer = new Customer();

22
common/cluster-api/src/main/java/org/thingsboard/server/queue/TbEdgeQueueAdmin.java

@ -0,0 +1,22 @@
/**
* 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.queue;
public interface TbEdgeQueueAdmin extends TbQueueAdmin {
void syncEdgeNotificationsOffsets(String fatGroupId, String newGroupId);
void deleteConsumerGroup(String consumerGroupId);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java

@ -66,6 +66,8 @@ public interface AssetService extends EntityDaoService {
PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(PageLink pageLink);
PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(TenantId tenantId, PageLink pageLink);
PageData<AssetId> findAssetIdsByTenantIdAndAssetProfileId(TenantId tenantId, AssetProfileId assetProfileId, PageLink pageLink);
ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds);

4
common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java

@ -39,6 +39,8 @@ public interface CalculatedFieldService extends EntityDaoService {
PageData<CalculatedField> findAllCalculatedFields(PageLink pageLink);
PageData<CalculatedField> findCalculatedFieldsByTenantId(TenantId tenantId, PageLink pageLink);
PageData<CalculatedField> findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink);
void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId);
@ -53,6 +55,8 @@ public interface CalculatedFieldService extends EntityDaoService {
List<CalculatedFieldLink> findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink);
boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -76,6 +76,8 @@ public interface DeviceService extends EntityDaoService {
PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(PageLink pageLink);
PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(TenantId tenantId, PageLink pageLink);
PageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
PageData<DeviceId> findDeviceIdsByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java

@ -47,6 +47,8 @@ public interface EdgeService extends EntityDaoService {
Optional<Edge> findEdgeByRoutingKey(TenantId tenantId, String routingKey);
PageData<Edge> findActiveEdges(PageLink pageLink);
Edge saveEdge(Edge edge);
Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId);

2
common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java

@ -125,7 +125,7 @@ public abstract class BaseEntityData<T extends EntityFields> implements EntityDa
return switch (key) {
case "createdTime" -> new LongDataPoint(System.currentTimeMillis(), fields.getCreatedTime());
case "edgeTemplate" -> new BoolDataPoint(System.currentTimeMillis(), fields.isEdgeTemplate());
case "parentId" -> new StringDataPoint(System.currentTimeMillis(), getRelatedParentId(ctx));
case "parentId" -> new StringDataPoint(System.currentTimeMillis(), getRelatedParentId(ctx), false);
default -> new StringDataPoint(System.currentTimeMillis(), getField(key), false);
};
}

6
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedJsonDataPoint.java

@ -17,10 +17,12 @@ package org.thingsboard.server.edqs.data.dp;
import org.thingsboard.server.common.data.kv.DataType;
import java.util.function.Function;
public class CompressedJsonDataPoint extends CompressedStringDataPoint {
public CompressedJsonDataPoint(long ts, byte[] compressedValue) {
super(ts, compressedValue);
public CompressedJsonDataPoint(long ts, byte[] compressedValue, Function<byte[], String> uncompressor) {
super(ts, compressedValue, uncompressor);
}
@Override

10
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java

@ -19,17 +19,21 @@ import lombok.Getter;
import lombok.SneakyThrows;
import org.thingsboard.common.util.TbBytePool;
import org.thingsboard.server.common.data.kv.DataType;
import org.xerial.snappy.Snappy;
import java.util.function.Function;
public class CompressedStringDataPoint extends AbstractDataPoint {
@Getter
private final byte[] compressedValue;
protected final Function<byte[], String> uncompressor;
@SneakyThrows
public CompressedStringDataPoint(long ts, byte[] compressedValue) {
public CompressedStringDataPoint(long ts, byte[] compressedValue, Function<byte[], String> uncompressor) {
super(ts);
this.compressedValue = TbBytePool.intern(compressedValue);
this.uncompressor = uncompressor;
}
@Override
@ -40,7 +44,7 @@ public class CompressedStringDataPoint extends AbstractDataPoint {
@SneakyThrows
@Override
public String getStr() {
return Snappy.uncompressString(compressedValue);
return uncompressor.apply(compressedValue);
}
@Override

2
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -139,7 +139,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
}
consumer.commit();
})
.consumerCreator((config, partitionId) -> queueFactory.createEdqsEventsConsumer())
.consumerCreator((config, tpi) -> queueFactory.createEdqsEventsConsumer())
.queueAdmin(queueFactory.getEdqsQueueAdmin())
.consumerExecutor(consumersExecutor)
.taskExecutor(taskExecutor)

5
common/edqs/src/main/java/org/thingsboard/server/edqs/query/processor/AbstractQueryProcessor.java

@ -64,8 +64,9 @@ public abstract class AbstractQueryProcessor<T extends EntityFilter> implements
}
protected static boolean checkCustomerId(UUID customerId, EntityData<?> ed) {
return customerId.equals(ed.getCustomerId()) || (ed.getEntityType() == EntityType.DASHBOARD &&
ed.getFields().getAssignedCustomerIds().contains(customerId));
return customerId.equals(ed.getCustomerId())
|| (ed.getEntityType() == EntityType.DASHBOARD && ed.getFields().getAssignedCustomerIds().contains(customerId))
|| (ed.getEntityType() == EntityType.CUSTOMER && customerId.equals(ed.getId()));
}
protected boolean matches(EntityData<?> ed) {

9
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java

@ -16,6 +16,7 @@
package org.thingsboard.server.edqs.repo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ObjectType;
@ -40,6 +41,7 @@ import java.util.function.Predicate;
@Slf4j
public class DefaultEdqsRepository implements EdqsRepository {
@Getter
private final static ConcurrentMap<TenantId, TenantRepo> repos = new ConcurrentHashMap<>();
private final EdqsStatsService statsService;
@ -52,6 +54,7 @@ public class DefaultEdqsRepository implements EdqsRepository {
if (event.getEventType() == EdqsEventType.DELETED && event.getObjectType() == ObjectType.TENANT) {
log.info("Tenant {} deleted", event.getTenantId());
repos.remove(event.getTenantId());
statsService.reportRemoved(ObjectType.TENANT);
} else {
get(event.getTenantId()).processEvent(event);
}
@ -61,8 +64,7 @@ public class DefaultEdqsRepository implements EdqsRepository {
public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck) {
long startNs = System.nanoTime();
long result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck);
double timingMs = (double) (System.nanoTime() - startNs) / 1000_000;
log.info("countEntitiesByQuery done in {} ms", timingMs);
statsService.reportEdqsCountQuery(tenantId, query, System.nanoTime() - startNs);
return result;
}
@ -71,8 +73,7 @@ public class DefaultEdqsRepository implements EdqsRepository {
EntityDataQuery query, boolean ignorePermissionCheck) {
long startNs = System.nanoTime();
var result = get(tenantId).findEntityDataByQuery(customerId, query, ignorePermissionCheck);
double timingMs = (double) (System.nanoTime() - startNs) / 1000_000;
log.info("findEntityDataByQuery done in {} ms", timingMs);
statsService.reportEdqsDataQuery(tenantId, query, System.nanoTime() - startNs);
return result;
}

2
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java

@ -332,7 +332,6 @@ public class TenantRepo {
public PageData<QueryResult> findEntityDataByQuery(CustomerId customerId, EntityDataQuery oldQuery, boolean ignorePermissionCheck) {
EdqsDataQuery query = RepositoryUtils.toNewQuery(oldQuery);
log.info("[{}][{}] findEntityDataByQuery: {}", tenantId, customerId, query);
QueryContext ctx = buildContext(customerId, query.getEntityFilter(), ignorePermissionCheck);
EntityQueryProcessor queryProcessor = EntityQueryProcessorFactory.create(this, ctx, query);
return sortAndConvert(query, queryProcessor.processQuery(), ctx);
@ -340,7 +339,6 @@ public class TenantRepo {
public long countEntitiesByQuery(CustomerId customerId, EntityCountQuery oldQuery, boolean ignorePermissionCheck) {
EdqsQuery query = RepositoryUtils.toNewQuery(oldQuery);
log.info("[{}][{}] countEntitiesByQuery: {}", tenantId, customerId, query);
QueryContext ctx = buildContext(customerId, query.getEntityFilter(), ignorePermissionCheck);
EntityQueryProcessor queryProcessor = EntityQueryProcessorFactory.create(this, ctx, query);
return queryProcessor.count();

5
common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java

@ -59,7 +59,8 @@ public class KafkaEdqsStateService implements EdqsStateService {
private final EdqsConfig config;
private final EdqsPartitionService partitionService;
private final KafkaEdqsQueueFactory queueFactory;
@Autowired @Lazy
@Autowired
@Lazy
private EdqsProcessor edqsProcessor;
private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> stateConsumer;
@ -93,7 +94,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
}
consumer.commit();
})
.consumerCreator((config, partitionId) -> queueFactory.createEdqsStateConsumer())
.consumerCreator((config, tpi) -> queueFactory.createEdqsStateConsumer())
.queueAdmin(queueAdmin)
.consumerExecutor(eventConsumer.getConsumerExecutor())
.taskExecutor(eventConsumer.getTaskExecutor())

89
common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java

@ -15,78 +15,117 @@
*/
package org.thingsboard.server.edqs.stats;
import jakarta.annotation.PostConstruct;
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.TbBytePool;
import org.thingsboard.common.util.TbStringPool;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsTimer;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.edqs.repo.DefaultEdqsRepository;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Service
@Slf4j
@RequiredArgsConstructor
@ConditionalOnExpression("'${queue.edqs.api.supported:true}' == 'true' && '${queue.edqs.stats.enabled:true}' == 'true'")
public class DefaultEdqsStatsService implements EdqsStatsService {
private final StatsFactory statsFactory;
@Value("${queue.edqs.stats.slow_query_threshold:3000}")
@Value("${queue.edqs.stats.slow_query_threshold}")
private int slowQueryThreshold;
private final ConcurrentHashMap<ObjectType, AtomicInteger> objectCounters = new ConcurrentHashMap<>();
private final StatsTimer dataQueryTimer;
private final StatsTimer countQueryTimer;
private final ConcurrentMap<ObjectType, AtomicInteger> objectCounters = new ConcurrentHashMap<>();
private final ConcurrentMap<String, StatsTimer> timers = new ConcurrentHashMap<>();
private final ConcurrentMap<String, StatsCounter> counters = new ConcurrentHashMap<>();
private DefaultEdqsStatsService(StatsFactory statsFactory) {
this.statsFactory = statsFactory;
dataQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityDataQueryTimer");
countQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityCountQueryTimer");
@PostConstruct
private void init() {
statsFactory.createGauge("edqsMapGauges", "stringPoolSize", TbStringPool.getPool(), Map::size);
statsFactory.createGauge("edqsMapGauges", "bytePoolSize", TbBytePool.getPool(), Map::size);
statsFactory.createGauge("edqsMapGauges", "tenantReposSize", DefaultEdqsRepository.getRepos(), Map::size);
}
@Override
public void reportAdded(ObjectType objectType) {
getObjectCounter(objectType).incrementAndGet();
getObjectGauge(objectType).incrementAndGet();
}
@Override
public void reportRemoved(ObjectType objectType) {
getObjectCounter(objectType).decrementAndGet();
getObjectGauge(objectType).decrementAndGet();
}
@Override
public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {
double timingMs = timingNanos / 1000_000.0;
if (timingMs < slowQueryThreshold) {
log.debug("[{}] Executed data query in {} ms: {}", tenantId, timingMs, query);
} else {
log.warn("[{}] Executed slow data query in {} ms: {}", tenantId, timingMs, query);
}
dataQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS);
public void reportEntityDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {
checkTiming(tenantId, query, timingNanos);
getTimer("entityDataQueryTimer").record(timingNanos, TimeUnit.NANOSECONDS);
}
@Override
public void reportEntityCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {
checkTiming(tenantId, query, timingNanos);
getTimer("entityCountQueryTimer").record(timingNanos, TimeUnit.NANOSECONDS);
}
@Override
public void reportEdqsDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {
checkTiming(tenantId, query, timingNanos);
getTimer("edqsDataQueryTimer").record(timingNanos, TimeUnit.NANOSECONDS);
}
@Override
public void reportEdqsCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {
checkTiming(tenantId, query, timingNanos);
getTimer("edqsCountQueryTimer").record(timingNanos, TimeUnit.NANOSECONDS);
}
@Override
public void reportStringCompressed() {
getCounter("stringsCompressed").increment();
}
@Override
public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {
public void reportStringUncompressed() {
getCounter("stringsUncompressed").increment();
}
private void checkTiming(TenantId tenantId, EntityCountQuery query, long timingNanos) {
double timingMs = timingNanos / 1000_000.0;
String queryType = query instanceof EntityDataQuery ? "data" : "count";
if (timingMs < slowQueryThreshold) {
log.debug("[{}] Executed count query in {} ms: {}", tenantId, timingMs, query);
log.debug("[{}] Executed " + queryType + " query in {} ms: {}", tenantId, timingMs, query);
} else {
log.warn("[{}] Executed slow count query in {} ms: {}", tenantId, timingMs, query);
log.warn("[{}] Executed slow " + queryType + " query in {} ms: {}", tenantId, timingMs, query);
}
countQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS);
}
private AtomicInteger getObjectCounter(ObjectType objectType) {
private StatsTimer getTimer(String name) {
return timers.computeIfAbsent(name, __ -> statsFactory.createStatsTimer("edqsTimers", name));
}
private StatsCounter getCounter(String name) {
return counters.computeIfAbsent(name, __ -> statsFactory.createStatsCounter("edqsCounters", name));
}
private AtomicInteger getObjectGauge(ObjectType objectType) {
return objectCounters.computeIfAbsent(objectType, type ->
statsFactory.createGauge("edqsObjectsCount", new AtomicInteger(), "objectType", type.name()));
statsFactory.createGauge("edqsGauges", "objectsCount", new AtomicInteger(), "objectType", type.name()));
}
}

39
common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.edqs.data.dp.BoolDataPoint;
import org.thingsboard.server.edqs.data.dp.CompressedJsonDataPoint;
@ -56,14 +57,18 @@ import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto;
import org.xerial.snappy.Snappy;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Slf4j
public class EdqsConverter {
private final EdqsStatsService edqsStatsService;
@Value("${queue.edqs.string_compression_length_threshold:512}")
private int stringCompressionLengthThreshold;
@ -171,26 +176,44 @@ public class EdqsConverter {
} else if (proto.hasDoubleV()) {
return new DoubleDataPoint(ts, proto.getDoubleV());
} else if (proto.hasStringV()) {
return new StringDataPoint(ts, proto.getStringV());
String stringV = proto.getStringV();
if (stringV.length() < stringCompressionLengthThreshold) {
return new StringDataPoint(ts, stringV);
} else {
return new CompressedStringDataPoint(ts, compress(stringV), this::uncompress);
}
} else if (proto.hasCompressedStringV()) {
return new CompressedStringDataPoint(ts, proto.getCompressedStringV().toByteArray());
return new CompressedStringDataPoint(ts, proto.getCompressedStringV().toByteArray(), this::uncompress);
} else if (proto.hasJsonV()) {
return new JsonDataPoint(ts, proto.getJsonV());
String jsonV = proto.getJsonV();
if (jsonV.length() < stringCompressionLengthThreshold) {
return new JsonDataPoint(ts, jsonV);
} else {
return new CompressedJsonDataPoint(ts, compress(jsonV), this::uncompress);
}
} else if (proto.hasCompressedJsonV()) {
return new CompressedJsonDataPoint(ts, proto.getCompressedJsonV().toByteArray());
return new CompressedJsonDataPoint(ts, proto.getCompressedJsonV().toByteArray(), this::uncompress);
} else {
throw new IllegalArgumentException("Unsupported data point proto: " + proto);
}
}
@SneakyThrows
private static byte[] compress(String value) {
byte[] compressed = Snappy.compress(value);
// TODO: limit the size
log.debug("Compressed {} bytes to {} bytes", value.length(), compressed.length);
private byte[] compress(String value) {
byte[] compressed = Snappy.compress(value, StandardCharsets.UTF_8);
log.debug("Compressed {} chars to {} bytes", value.length(), compressed.length);
edqsStatsService.reportStringCompressed();
return compressed;
}
@SneakyThrows
private String uncompress(byte[] compressed) {
String value = Snappy.uncompressString(compressed, StandardCharsets.UTF_8);
log.debug("Uncompressed {} bytes to {} chars", compressed.length, value.length());
edqsStatsService.reportStringUncompressed();
return value;
}
public static Entity toEntity(EntityType entityType, Object entity) {
Entity edqsEntity = new Entity();
edqsEntity.setType(entityType);

2
common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java

@ -136,6 +136,8 @@ public enum MsgType {
EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG,
CF_CACHE_INIT_MSG, // Sent to init caches for CF actor;
CF_INIT_PROFILE_ENTITY_MSG, // Sent to init profile entities cache;
CF_INIT_MSG, // Sent to init particular calculated field;
CF_LINK_INIT_MSG, // Sent to init particular calculated field;
CF_STATE_RESTORE_MSG, // Sent to restore particular calculated field entity state;

33
common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldCacheInitMsg.java

@ -0,0 +1,33 @@
/**
* 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.msg.cf;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@Data
public class CalculatedFieldCacheInitMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
@Override
public MsgType getMsgType() {
return MsgType.CF_CACHE_INIT_MSG;
}
}

26
application/src/main/java/org/thingsboard/server/service/cf/cache/CalculatedFieldEntityProfileCache.java → common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitProfileEntityMsg.java

@ -13,24 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf.cache;
package org.thingsboard.server.common.msg.cf;
import org.springframework.context.ApplicationListener;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import java.util.Collection;
@Data
public class CalculatedFieldInitProfileEntityMsg implements ToCalculatedFieldSystemMsg {
public interface CalculatedFieldEntityProfileCache extends ApplicationListener<PartitionChangeEvent> {
private final TenantId tenantId;
private final EntityId profileEntityId;
private final EntityId entityId;
void add(TenantId tenantId, EntityId profileId, EntityId entityId);
@Override
public MsgType getMsgType() {
return MsgType.CF_INIT_PROFILE_ENTITY_MSG;
}
void update(TenantId tenantId, EntityId oldProfileId, EntityId newProfileId, EntityId entityId);
void evict(TenantId tenantId, EntityId entityId);
Collection<EntityId> getMyEntityIdsByProfileId(TenantId tenantId, EntityId profileId);
int getEntityIdPartition(TenantId tenantId, EntityId entityId);
}

6
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java

@ -54,7 +54,7 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
@Getter
protected C config;
protected final MsgPackProcessor<M, C> msgPackProcessor;
protected final BiFunction<C, Integer, TbQueueConsumer<M>> consumerCreator;
protected final BiFunction<C, TopicPartitionInfo, TbQueueConsumer<M>> consumerCreator;
@Getter
protected final ExecutorService consumerExecutor;
@Getter
@ -74,7 +74,7 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
@Builder
public MainQueueConsumerManager(QueueKey queueKey, C config,
MsgPackProcessor<M, C> msgPackProcessor,
BiFunction<C, Integer, TbQueueConsumer<M>> consumerCreator,
BiFunction<C, TopicPartitionInfo, TbQueueConsumer<M>> consumerCreator,
ExecutorService consumerExecutor,
ScheduledExecutorService scheduler,
ExecutorService taskExecutor,
@ -313,7 +313,7 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null;
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, () -> {
TbQueueConsumer<M> queueConsumer = consumerCreator.apply(config, partitionId);
TbQueueConsumer<M> queueConsumer = consumerCreator.apply(config, tpi);
if (startOffsetProvider != null && queueConsumer instanceof TbKafkaConsumerTemplate<M> kafkaConsumer) {
kafkaConsumer.setStartOffsetProvider(startOffsetProvider);
}

2
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java

@ -45,7 +45,7 @@ public class PartitionedQueueConsumerManager<M extends TbQueueMsg> extends MainQ
@Builder(builderMethodName = "create") // not to conflict with super.builder()
public PartitionedQueueConsumerManager(QueueKey queueKey, String topic, long pollInterval, MsgPackProcessor<M, QueueConfig> msgPackProcessor,
BiFunction<QueueConfig, Integer, TbQueueConsumer<M>> consumerCreator, TbQueueAdmin queueAdmin,
BiFunction<QueueConfig, TopicPartitionInfo, TbQueueConsumer<M>> consumerCreator, TbQueueAdmin queueAdmin,
ExecutorService consumerExecutor, ScheduledExecutorService scheduler,
ExecutorService taskExecutor, Consumer<Throwable> uncaughtErrorHandler) {
super(queueKey, QueueConfig.of(true, pollInterval), msgPackProcessor, consumerCreator, consumerExecutor, scheduler, taskExecutor, uncaughtErrorHandler);

2
common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java

@ -38,7 +38,7 @@ public class EdqsConfig {
private String requestsTopic;
@Value("${queue.edqs.responses_topic:edqs.responses}")
private String responsesTopic;
@Value("${queue.edqs.poll_interval:125}")
@Value("${queue.edqs.poll_interval:25}")
private long pollInterval;
@Value("${queue.edqs.max_pending_requests:10000}")
private int maxPendingRequests;

36
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java

@ -26,6 +26,7 @@ import org.apache.kafka.clients.admin.TopicDescription;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.TopicExistsException;
import org.thingsboard.server.queue.TbEdgeQueueAdmin;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.util.PropertyUtils;
@ -43,7 +44,7 @@ import java.util.stream.Collectors;
* Created by ashvayka on 24.09.18.
*/
@Slf4j
public class TbKafkaAdmin implements TbQueueAdmin {
public class TbKafkaAdmin implements TbQueueAdmin, TbEdgeQueueAdmin {
private final TbKafkaSettings settings;
private final Map<String, String> topicConfigs;
@ -149,17 +150,38 @@ public class TbKafkaAdmin implements TbQueueAdmin {
* */
public void syncOffsets(String fatGroupId, String newGroupId, Integer partitionId) {
try {
syncOffsetsUnsafe(fatGroupId, newGroupId, partitionId);
log.info("syncOffsets [{}][{}][{}]", fatGroupId, newGroupId, partitionId);
if (partitionId == null) {
return;
}
syncOffsetsUnsafe(fatGroupId, newGroupId, "." + partitionId);
} catch (Exception e) {
log.warn("Failed to syncOffsets from {} to {} partitionId {}", fatGroupId, newGroupId, partitionId, e);
}
}
void syncOffsetsUnsafe(String fatGroupId, String newGroupId, Integer partitionId) throws ExecutionException, InterruptedException, TimeoutException {
log.info("syncOffsets [{}][{}][{}]", fatGroupId, newGroupId, partitionId);
if (partitionId == null) {
return;
/**
* Sync edge notifications offsets from a fat group to a single group per edge
* */
public void syncEdgeNotificationsOffsets(String fatGroupId, String newGroupId) {
try {
log.info("syncEdgeNotificationsOffsets [{}][{}]", fatGroupId, newGroupId);
syncOffsetsUnsafe(fatGroupId, newGroupId, newGroupId);
} catch (Exception e) {
log.warn("Failed to syncEdgeNotificationsOffsets from {} to {}", fatGroupId, newGroupId, e);
}
}
@Override
public void deleteConsumerGroup(String consumerGroupId) {
try {
settings.getAdminClient().deleteConsumerGroups(Collections.singletonList(consumerGroupId));
} catch (Exception e) {
log.warn("Failed to delete consumer group {}", consumerGroupId, e);
}
}
void syncOffsetsUnsafe(String fatGroupId, String newGroupId, String topicSuffix) throws ExecutionException, InterruptedException, TimeoutException {
Map<TopicPartition, OffsetAndMetadata> oldOffsets = getConsumerGroupOffsets(fatGroupId);
if (oldOffsets.isEmpty()) {
return;
@ -167,7 +189,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
for (var consumerOffset : oldOffsets.entrySet()) {
var tp = consumerOffset.getKey();
if (!tp.topic().endsWith("." + partitionId)) {
if (!tp.topic().endsWith(topicSuffix)) {
continue;
}
var om = consumerOffset.getValue();

3
common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java

@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
@ -133,7 +134,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer() {
public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer(TopicPartitionInfo tpi) {
return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(calculatedFieldSettings.getEventTopic()));
}

30
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java

@ -20,10 +20,12 @@ import jakarta.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg;
@ -44,6 +46,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
import org.thingsboard.server.queue.TbEdgeQueueAdmin;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueProducer;
@ -101,7 +104,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
private final TbQueueAdmin vcAdmin;
private final TbQueueAdmin housekeeperAdmin;
private final TbQueueAdmin housekeeperReprocessingAdmin;
private final TbQueueAdmin edgeAdmin;
private final TbEdgeQueueAdmin edgeAdmin;
private final TbQueueAdmin edgeEventAdmin;
private final TbQueueAdmin cfAdmin;
private final TbQueueAdmin cfStateAdmin;
@ -493,9 +496,13 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
public TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic());
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic();
edgeAdmin.syncEdgeNotificationsOffsets(topicService.buildTopicName("monolith-edge-event-consumer"), topic);
consumerBuilder.topic(topic);
consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer"));
consumerBuilder.groupId(topic);
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(edgeEventAdmin);
consumerBuilder.statsService(consumerStatsService);
@ -513,15 +520,24 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer() {
public TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer(TopicPartitionInfo tpi) {
String queueName = DataConstants.CF_QUEUE_NAME;
if (tpi == null) {
throw new IllegalArgumentException("TopicPartitionInfo is required.");
}
TenantId tenantId = tpi.getTenantId().orElse(TenantId.SYS_TENANT_ID);
Integer partitionId = tpi.getPartition().orElseThrow(() -> new IllegalArgumentException("PartitionId is required."));
String groupId = topicService.buildConsumerGroupId("cf-", tenantId, queueName, partitionId);
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName(calculatedFieldSettings.getEventTopic()));
consumerBuilder.clientId("monolith-calculated-field-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-calculated-field-consumer"));
consumerBuilder.clientId("cf-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(groupId);
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCalculatedFieldMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(cfAdmin);
consumerBuilder.statsService(consumerStatsService);
return consumerBuilder.build();
}
@ -571,7 +587,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
.readFromBeginning(true)
.stopWhenRead(true)
.clientId("monolith-calculated-field-state-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet())
.groupId(topicService.buildTopicName("monolith-calculated-field-state-consumer"))
.groupId(null) // not using consumer group management
.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), msg.getData() != null ? CalculatedFieldStateProto.parseFrom(msg.getData()) : null, msg.getHeaders()))
.admin(cfStateAdmin)
.statsService(consumerStatsService)

11
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java

@ -42,6 +42,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
import org.thingsboard.server.queue.TbEdgeQueueAdmin;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueProducer;
@ -99,7 +100,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
private final TbQueueAdmin vcAdmin;
private final TbQueueAdmin housekeeperAdmin;
private final TbQueueAdmin housekeeperReprocessingAdmin;
private final TbQueueAdmin edgeAdmin;
private final TbEdgeQueueAdmin edgeAdmin;
private final TbQueueAdmin edgeEventAdmin;
private final TbQueueAdmin cfAdmin;
private final TbQueueAdmin edqsEventsAdmin;
@ -439,9 +440,13 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
public TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic());
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic();
edgeAdmin.syncEdgeNotificationsOffsets(topicService.buildTopicName("tb-core-edge-event-consumer"), topic);
consumerBuilder.topic(topic);
consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer"));
consumerBuilder.groupId(topic);
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(edgeEventAdmin);
consumerBuilder.statsService(consumerStatsService);

19
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java

@ -20,8 +20,11 @@ import jakarta.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg;
@ -313,12 +316,20 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory {
}
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer() {
public TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer(TopicPartitionInfo tpi) {
String queueName = DataConstants.CF_QUEUE_NAME;
if (tpi == null) {
throw new IllegalArgumentException("TopicPartitionInfo is required.");
}
TenantId tenantId = tpi.getTenantId().orElse(TenantId.SYS_TENANT_ID);
Integer partitionId = tpi.getPartition().orElseThrow(() -> new IllegalArgumentException("PartitionId is required."));
String groupId = topicService.buildConsumerGroupId("cf-", tenantId, queueName, partitionId);
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName(calculatedFieldSettings.getEventTopic()));
consumerBuilder.clientId("tb-rule-engine-calculated-field-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-rule-engine-calculated-field-consumer"));
consumerBuilder.clientId("cf-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(groupId);
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCalculatedFieldMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(cfAdmin);
consumerBuilder.statsService(consumerStatsService);
@ -371,7 +382,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory {
.readFromBeginning(true)
.stopWhenRead(true)
.clientId("tb-rule-engine-calculated-field-state-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet())
.groupId(topicService.buildTopicName("tb-rule-engine-calculated-field-state-consumer"))
.groupId(null) // not using consumer group management
.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), msg.getData() != null ? CalculatedFieldStateProto.parseFrom(msg.getData()) : null, msg.getHeaders()))
.admin(cfStateAdmin)
.statsService(consumerStatsService)

9
common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java

@ -16,6 +16,7 @@
package org.thingsboard.server.queue.provider;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg;
@ -95,8 +96,8 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory
/**
* Used to consume messages by TB Rule Engine Service
*
* @return
* @param configuration
* @return
*/
TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> createToRuleEngineMsgConsumer(Queue configuration);
@ -104,9 +105,9 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory
* Used to consume messages by TB Rule Engine Service
* Intended usage for consumer per partition strategy
*
* @return TbQueueConsumer
* @param configuration
* @param partitionId as a suffix for consumer name
* @param partitionId as a suffix for consumer name
* @return TbQueueConsumer
*/
default TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> createToRuleEngineMsgConsumer(Queue configuration, Integer partitionId) {
return createToRuleEngineMsgConsumer(configuration);
@ -121,7 +122,7 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory
TbQueueRequestTemplate<TbProtoJsQueueMsg<JsInvokeProtos.RemoteJsRequest>, TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> createRemoteJsRequestTemplate();
TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer();
TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer(TopicPartitionInfo tpi);
TbQueueAdmin getCalculatedFieldQueueAdmin();

2
common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueCalculatedFieldSettings.java

@ -31,5 +31,7 @@ public class TbQueueCalculatedFieldSettings {
@Value("${queue.calculated_fields.state_topic}")
private String stateTopic;
@Value("${queue.calculated_fields.init_tenant_fetch_pack_size:1000}")
private int initTenantFetchPackSize;
}

6
common/queue/src/main/java/org/thingsboard/server/queue/util/AfterStartUp.java

@ -36,10 +36,10 @@ public @interface AfterStartUp {
int STARTUP_SERVICE = 8;
int ACTOR_SYSTEM = 9;
int REGULAR_SERVICE = 10;
int CF_READ_PROFILE_ENTITIES_SERVICE = 10;
int CF_READ_CF_SERVICE = 11;
int CF_READ_CF_SERVICE = 10;
int REGULAR_SERVICE = 11;
int BEFORE_TRANSPORT_SERVICE = Integer.MAX_VALUE - 1001;
int TRANSPORT_SERVICE = Integer.MAX_VALUE - 1000;

15
common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java

@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.ToDoubleFunction;
@Service
public class DefaultStatsFactory implements StatsFactory {
@ -86,6 +87,16 @@ public class DefaultStatsFactory implements StatsFactory {
return meterRegistry.gauge(key, Tags.of(tags), number);
}
@Override
public <T extends Number> T createGauge(String type, String name, T number, String... tags) {
return createGauge(type, number, getTags(name, tags));
}
@Override
public <S> void createGauge(String type, String name, S stateObject, ToDoubleFunction<S> numberProvider, String... tags) {
meterRegistry.gauge(type, Tags.of(getTags(name, tags)), stateObject, numberProvider);
}
@Override
public MessagesStats createMessagesStats(String key) {
StatsCounter totalCounter = createStatsCounter(key, TOTAL_MSGS);
@ -106,8 +117,8 @@ public class DefaultStatsFactory implements StatsFactory {
}
@Override
public StatsTimer createTimer(StatsType type, String name, String... tags) {
return new StatsTimer(name, Timer.builder(type.getName())
public StatsTimer createStatsTimer(String type, String name, String... tags) {
return new StatsTimer(name, Timer.builder(type)
.tags(getTags(name, tags))
.register(meterRegistry));
}

16
common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java

@ -33,9 +33,21 @@ public class DummyEdqsStatsService implements EdqsStatsService {
public void reportRemoved(ObjectType objectType) {}
@Override
public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {}
public void reportEntityDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {}
@Override
public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {}
public void reportEntityCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {}
@Override
public void reportEdqsDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {}
@Override
public void reportEdqsCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {}
@Override
public void reportStringCompressed() {}
@Override
public void reportStringUncompressed() {}
}

12
common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java

@ -26,8 +26,16 @@ public interface EdqsStatsService {
void reportRemoved(ObjectType objectType);
void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos);
void reportEntityDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos);
void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos);
void reportEntityCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos);
void reportEdqsDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos);
void reportEdqsCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos);
void reportStringCompressed();
void reportStringUncompressed();
}

8
common/stats/src/main/java/org/thingsboard/server/common/stats/StatsFactory.java

@ -17,6 +17,8 @@ package org.thingsboard.server.common.stats;
import io.micrometer.core.instrument.Timer;
import java.util.function.ToDoubleFunction;
public interface StatsFactory {
StatsCounter createStatsCounter(String key, String statsName, String... otherTags);
@ -25,10 +27,14 @@ public interface StatsFactory {
<T extends Number> T createGauge(String key, T number, String... tags);
<T extends Number> T createGauge(String type, String name, T number, String... tags);
<S> void createGauge(String type, String name, S stateObject, ToDoubleFunction<S> numberProvider, String... tags);
MessagesStats createMessagesStats(String key);
Timer createTimer(String key, String... tags);
StatsTimer createTimer(StatsType type, String name, String... tags);
StatsTimer createStatsTimer(String type, String name, String... tags);
}

6
common/util/src/main/java/org/thingsboard/common/util/TbBytePool.java

@ -16,12 +16,14 @@
package org.thingsboard.common.util;
import com.google.common.hash.Hashing;
import lombok.Getter;
import org.springframework.util.ConcurrentReferenceHashMap;
import java.util.concurrent.ConcurrentMap;
public class TbBytePool {
@Getter
private static final ConcurrentMap<String, byte[]> pool = new ConcurrentReferenceHashMap<>();
public static byte[] intern(byte[] data) {
@ -32,8 +34,4 @@ public class TbBytePool {
return pool.computeIfAbsent(checksum, c -> data);
}
public static int size(){
return pool.size();
}
}

6
common/util/src/main/java/org/thingsboard/common/util/TbStringPool.java

@ -15,12 +15,14 @@
*/
package org.thingsboard.common.util;
import lombok.Getter;
import org.springframework.util.ConcurrentReferenceHashMap;
import java.util.concurrent.ConcurrentMap;
public class TbStringPool {
@Getter
private static final ConcurrentMap<String, String> pool = new ConcurrentReferenceHashMap<>();
public static String intern(String data) {
@ -30,8 +32,4 @@ public class TbStringPool {
return pool.computeIfAbsent(data, str -> str);
}
public static int size(){
return pool.size();
}
}

2
dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java

@ -240,4 +240,6 @@ public interface AssetDao extends Dao<Asset>, TenantEntityDao<Asset>, Exportable
PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(PageLink pageLink);
PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, PageLink pageLink);
}

8
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -293,6 +293,14 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return assetDao.findProfileEntityIdInfos(pageLink);
}
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findProfileEntityIdInfosByTenantId, tenantId[{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validatePageLink(pageLink);
return assetDao.findProfileEntityIdInfosByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<AssetId> findAssetIdsByTenantIdAndAssetProfileId(TenantId tenantId, AssetProfileId assetProfileId, PageLink pageLink) {
log.trace("Executing findAssetIdsByTenantIdAndAssetProfileId, tenantId [{}], assetProfileId [{}]", tenantId, assetProfileId);

16
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -104,6 +104,14 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldDao.findAll(pageLink);
}
@Override
public PageData<CalculatedField> findCalculatedFieldsByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findAllByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validatePageLink(pageLink);
return calculatedFieldDao.findAllByTenantId(tenantId, pageLink);
}
@Override
public PageData<CalculatedField> findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) {
log.trace("Executing findAllByEntityId, entityId [{}], pageLink [{}]", entityId, pageLink);
@ -174,6 +182,14 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validatePageLink(pageLink);
return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink);

2
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java

@ -37,6 +37,8 @@ public interface CalculatedFieldDao extends Dao<CalculatedField> {
PageData<CalculatedField> findAll(PageLink pageLink);
PageData<CalculatedField> findAllByTenantId(TenantId tenantId, PageLink pageLink);
PageData<CalculatedField> findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink);
List<CalculatedField> removeAllByEntityId(TenantId tenantId, EntityId entityId);

4
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java

@ -31,8 +31,12 @@ public interface CalculatedFieldLinkDao extends Dao<CalculatedFieldLink> {
List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId);
List<CalculatedFieldLink> findAll();
PageData<CalculatedFieldLink> findAll(PageLink pageLink);
PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink);
}

4
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -23,9 +23,7 @@ import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
@ -233,6 +231,8 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao<Device>, Exporta
PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(PageLink pageLink);
PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink);
}

8
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -394,6 +394,14 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
return deviceDao.findProfileEntityIdInfos(pageLink);
}
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findProfileEntityIdInfosByTenantId, tenantId[{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validatePageLink(pageLink);
return deviceDao.findProfileEntityIdInfosByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);

2
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java

@ -41,6 +41,8 @@ public interface EdgeDao extends Dao<Edge>, TenantEntityDao<Edge> {
EdgeInfo findEdgeInfoById(TenantId tenantId, UUID edgeId);
PageData<Edge> findActiveEdges(PageLink pageLink);
PageData<EdgeId> findEdgeIdsByTenantId(UUID tenantId, PageLink pageLink);
PageData<Edge> findEdgesByTenantId(UUID tenantId, PageLink pageLink);

7
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -192,6 +192,13 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
return edgeDao.findByRoutingKey(tenantId.getId(), routingKey);
}
@Override
public PageData<Edge> findActiveEdges(PageLink pageLink) {
log.trace("Executing findActiveEdges [{}]", pageLink);
Validator.validatePageLink(pageLink);
return edgeDao.findActiveEdges(pageLink);
}
@Override
public Edge saveEdge(Edge edge) {
log.trace("Executing saveEdge [{}]", edge);

19
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasEmail;
@ -43,6 +42,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilterType;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.query.EntityNameFilter;
import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
@ -61,6 +61,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID;
import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_NAME;
import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_TYPE;
import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -101,7 +102,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id);
validateEntityCountQuery(query);
TbStopWatch stopWatch = TbStopWatch.create();
long startNs = System.nanoTime();
Long result;
if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) {
EdqsRequest request = EdqsRequest.builder()
@ -112,7 +113,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
} else {
result = entityQueryDao.countEntitiesByQuery(tenantId, customerId, query);
}
edqsStatsService.reportCountQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos());
edqsStatsService.reportEntityCountQuery(tenantId, query, System.nanoTime() - startNs);
return result;
}
@ -123,7 +124,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id);
validateEntityDataQuery(query);
TbStopWatch stopWatch = TbStopWatch.create();
long startNs = System.nanoTime();
PageData<EntityData> result;
if (edqsApiService.isEnabled() && validForEdqs(query)) {
EdqsRequest request = EdqsRequest.builder()
@ -146,7 +147,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
}
}
}
edqsStatsService.reportDataQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos());
edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs);
return result;
}
@ -250,6 +251,8 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateRelationQuery((RelationsQueryFilter) query.getEntityFilter());
} else if (query.getEntityFilter().getType().equals(ENTITY_TYPE)) {
validateEntityTypeQuery((EntityTypeFilter) query.getEntityFilter());
} else if (query.getEntityFilter().getType().equals(ENTITY_NAME)) {
validateEntityNameQuery((EntityNameFilter) query.getEntityFilter());
}
}
@ -264,6 +267,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
}
}
private static void validateEntityNameQuery(EntityNameFilter filter) {
if (filter.getEntityType() == null) {
throw new IncorrectParameterException("Entity type is required");
}
}
private static void validateRelationQuery(RelationsQueryFilter queryFilter) {
if (queryFilter.isMultiRoot() && queryFilter.getMultiRootEntitiesType() == null) {
throw new IncorrectParameterException("Multi-root relation query filter should contain 'multiRootEntitiesType'");

10
dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java

@ -39,14 +39,12 @@ import org.thingsboard.server.dao.model.sql.AssetEntity;
import org.thingsboard.server.dao.model.sql.AssetInfoEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.sql.device.NativeAssetRepository;
import org.thingsboard.server.dao.sql.device.NativeDeviceRepository;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto;
@ -262,10 +260,16 @@ public class JpaAssetDao extends JpaAbstractDao<AssetEntity, Asset> implements A
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(PageLink pageLink) {
log.debug("Find profile device id infos by pageLink [{}]", pageLink);
log.debug("Find profile asset id infos by pageLink [{}]", pageLink);
return nativeAssetRepository.findProfileEntityIdInfos(DaoUtil.toPageable(pageLink));
}
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, PageLink pageLink) {
log.debug("Find profile asset id infos by pageLink [{}]", pageLink);
return nativeAssetRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink));
}
@Override
public Long countByTenantId(TenantId tenantId) {
return assetRepository.countByTenantId(tenantId.getId());

6
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
@ -27,4 +29,8 @@ public interface CalculatedFieldLinkRepository extends JpaRepository<CalculatedF
List<CalculatedFieldLinkEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
List<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId);
Page<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId, Pageable pageable);
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java

@ -32,6 +32,8 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
List<CalculatedFieldEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
Page<CalculatedFieldEntity> findAllByTenantId(UUID tenantId, Pageable pageable);
Page<CalculatedFieldEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, Pageable pageable);
List<CalculatedFieldEntity> findAllByTenantId(UUID tenantId);

6
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -71,6 +71,12 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
return nativeCalculatedFieldRepository.findCalculatedFields(DaoUtil.toPageable(pageLink));
}
@Override
public PageData<CalculatedField> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
log.debug("Try to find calculated fields by tenantId[{}] and pageLink [{}]", tenantId, pageLink);
return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<CalculatedField> findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) {
log.debug("Try to find calculated fields by entityId[{}] and pageLink [{}]", entityId, pageLink);

11
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java

@ -54,6 +54,11 @@ public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLin
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId()));
}
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId()));
}
@Override
public List<CalculatedFieldLink> findAll() {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll());
@ -65,6 +70,12 @@ public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLin
return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink));
}
@Override
public PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink);
return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
protected Class<CalculatedFieldLinkEntity> getEntityClass() {
return CalculatedFieldLinkEntity.class;

17
dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java

@ -20,12 +20,9 @@ import org.springframework.data.domain.Pageable;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -43,8 +40,8 @@ public class DefaultNativeAssetRepository extends AbstractNativeRepository imple
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(Pageable pageable) {
String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s";
return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, row -> {
String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s";
return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, row -> {
AssetId id = new AssetId((UUID) row.get("id"));
AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId"));
var tenantIdObj = row.get("tenantId");
@ -52,4 +49,14 @@ public class DefaultNativeAssetRepository extends AbstractNativeRepository imple
});
}
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) {
String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId);
return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, row -> {
AssetId id = new AssetId((UUID) row.get("id"));
AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId"));
var tenantIdObj = row.get("tenantId");
return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id);
});
}
}

11
dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java

@ -61,4 +61,15 @@ public class DefaultNativeDeviceRepository extends AbstractNativeRepository impl
});
}
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) {
String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId);
return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, row -> {
DeviceId id = new DeviceId((UUID) row.get("id"));
DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId"));
var tenantIdObj = row.get("tenantId");
return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id);
});
}
}

16
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java

@ -179,11 +179,11 @@ public class JpaDeviceDao extends JpaAbstractDao<DeviceEntity, Device> implement
@Override
public PageData<DeviceId> findDeviceIdsByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink) {
return DaoUtil.pageToPageData(
deviceRepository.findIdsByTenantIdAndDeviceProfileId(
tenantId,
deviceProfileId,
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)))
deviceRepository.findIdsByTenantIdAndDeviceProfileId(
tenantId,
deviceProfileId,
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)))
.mapData(DeviceId::new);
}
@ -281,6 +281,12 @@ public class JpaDeviceDao extends JpaAbstractDao<DeviceEntity, Device> implement
return nativeDeviceRepository.findProfileEntityIdInfos(DaoUtil.toPageable(pageLink));
}
@Override
public PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, PageLink pageLink) {
log.debug("Find profile device id infos by tenantId[{}], pageLink [{}]", tenantId, pageLink);
return nativeDeviceRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink));
}
@Override
public Device findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(deviceRepository.findByTenantIdAndExternalId(tenantId, externalId));

4
dao/src/main/java/org/thingsboard/server/dao/sql/device/NativeProfileEntityRepository.java

@ -19,8 +19,12 @@ import org.springframework.data.domain.Pageable;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.page.PageData;
import java.util.UUID;
public interface NativeProfileEntityRepository {
PageData<ProfileEntityIdInfo> findProfileEntityIdInfos(Pageable pageable);
PageData<ProfileEntityIdInfo> findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable);
}

12
dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java

@ -44,6 +44,18 @@ public interface EdgeRepository extends JpaRepository<EdgeEntity, UUID> {
"WHERE d.id = :edgeId")
EdgeInfoEntity findEdgeInfoById(@Param("edgeId") UUID edgeId);
@Query(value = "SELECT ee.id, ee.created_time, ee.additional_info, ee.customer_id, " +
"ee.root_rule_chain_id, ee.type, ee.name, ee.label, ee.routing_key, " +
"ee.secret, ee.tenant_id, ee.version " +
"FROM edge ee " +
"JOIN attribute_kv ON ee.id = attribute_kv.entity_id " +
"JOIN key_dictionary ON attribute_kv.attribute_key = key_dictionary.key_id " +
"WHERE attribute_kv.bool_v = true AND key_dictionary.key = 'active' " +
"AND (:textSearch IS NULL OR ee.name ILIKE CONCAT('%', :textSearch, '%')) " +
"ORDER BY ee.id", nativeQuery = true)
Page<EdgeEntity> findActiveEdges(@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d.id FROM EdgeEntity d WHERE d.tenantId = :tenantId " +
"AND (:textSearch IS NULL OR ilike(d.name, CONCAT('%', :textSearch, '%')) = true)")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId,

8
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java

@ -66,6 +66,14 @@ public class JpaEdgeDao extends JpaAbstractDao<EdgeEntity, Edge> implements Edge
return DaoUtil.getData(edgeRepository.findEdgeInfoById(edgeId));
}
@Override
public PageData<Edge> findActiveEdges(PageLink pageLink) {
return DaoUtil.toPageData(
edgeRepository.findActiveEdges(
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<EdgeId> findEdgeIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(

2
docker/compose-utils.sh

@ -134,7 +134,7 @@ function additionalComposeEdqsArgs() {
if [ "$EDQS_ENABLED" = true ]
then
ADDITIONAL_COMPOSE_EDQS_ARGS="-f docker-compose.edqs.yml"
echo ADDITIONAL_COMPOSE_EDQS_ARGS
echo $ADDITIONAL_COMPOSE_EDQS_ARGS
else
echo ""
fi

6
docker/docker-install-tb.sh

@ -56,8 +56,7 @@ ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $?
if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then
COMPOSE_ARGS="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
up -d ${ADDITIONAL_STARTUP_SERVICES}"
case $COMPOSE_VERSION in
@ -74,8 +73,7 @@ if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then
fi
COMPOSE_ARGS="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=${loadDemo} \
tb-core1"

6
docker/docker-update-service.sh

@ -30,13 +30,11 @@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $?
COMPOSE_ARGS_PULL="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
pull"
COMPOSE_ARGS_BUILD="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
up -d --no-deps --build"
case $COMPOSE_VERSION in

9
docker/docker-upgrade-tb.sh

@ -47,19 +47,16 @@ ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $?
ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $?
COMPOSE_ARGS_PULL="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
pull \
tb-core1"
COMPOSE_ARGS_UP="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
up -d ${ADDITIONAL_STARTUP_SERVICES}"
COMPOSE_ARGS_RUN="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
run --no-deps --rm -e UPGRADE_TB=true -e FROM_VERSION=${fromVersion} \
tb-core1"

1375
docker/monitoring/grafana/provisioning/dashboards/edqs.json

File diff suppressed because it is too large

161
docker/monitoring/grafana/provisioning/dashboards/edqs_entities.json

@ -1,161 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 6,
"iteration": 1737564772936,
"links": [],
"liveNow": false,
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 1,
"pointSize": 1,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "9BonzvTSz"
},
"exemplar": true,
"expr": "sum by (objectType) (edqs_object_count{tenantId=~\"$tenantId\"})",
"interval": "",
"legendFormat": "{{objectType}}",
"refId": "A"
}
],
"title": "EDQS object count",
"type": "timeseries"
}
],
"refresh": "",
"schemaVersion": 35,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"definition": "label_values(edqs_object_count, tenantId)",
"hide": 0,
"includeAll": true,
"label": "Tenant",
"multi": true,
"name": "tenantId",
"options": [],
"query": {
"query": "label_values(edqs_object_count, tenantId)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-15m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "EDQS",
"uid": "mK5A_DdHk",
"version": 9,
"weekStart": ""
}

4
edqs/src/main/resources/edqs.yml

@ -66,7 +66,7 @@ queue:
# EDQS responses topic
responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}"
# Poll interval for EDQS topics
poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}"
poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:25}"
# Maximum amount of pending requests to EDQS
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
# Maximum timeout for requests to EDQS
@ -76,6 +76,8 @@ queue:
stats:
# Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}"
# Threshold for slow queries to log, in milliseconds
slow_query_threshold: "${TB_EDQS_SLOW_QUERY_THRESHOLD_MS:200}"
kafka:
# Kafka Bootstrap nodes in "host:port" format

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java

@ -80,7 +80,7 @@ class ProfileState {
addEntityKeysFromAlarmConditionSpec(alarmRule);
AlarmSchedule schedule = alarmRule.getSchedule();
if (schedule != null) {
addScheduleDynamicValues(schedule);
addScheduleDynamicValues(schedule, entityKeys);
}
}));
if (alarm.getClearRule() != null) {
@ -96,9 +96,9 @@ class ProfileState {
}
}
private void addScheduleDynamicValues(AlarmSchedule schedule) {
void addScheduleDynamicValues(AlarmSchedule schedule, final Set<AlarmConditionFilterKey> entityKeys) {
DynamicValue<String> dynamicValue = schedule.getDynamicValue();
if (dynamicValue != null) {
if (dynamicValue != null && dynamicValue.getSourceAttribute() != null) {
entityKeys.add(
new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE,
dynamicValue.getSourceAttribute())
@ -137,13 +137,14 @@ class ProfileState {
}
private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<AlarmConditionFilterKey> entityKeys, Set<AlarmConditionFilterKey> ruleKeys) {
void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<AlarmConditionFilterKey> entityKeys, Set<AlarmConditionFilterKey> ruleKeys) {
switch (predicate.getType()) {
case STRING:
case NUMERIC:
case BOOLEAN:
DynamicValue value = ((SimpleKeyFilterPredicate) predicate).getValue().getDynamicValue();
if (value != null && (value.getSourceType() == DynamicValueSourceType.CURRENT_TENANT ||
if (value != null && value.getSourceAttribute() != null && (
value.getSourceType() == DynamicValueSourceType.CURRENT_TENANT ||
value.getSourceType() == DynamicValueSourceType.CURRENT_CUSTOMER ||
value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE)) {
AlarmConditionFilterKey entityKey = new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, value.getSourceAttribute());

127
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/ProfileStateTest.java

@ -0,0 +1,127 @@
/**
* 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.rule.engine.profile;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.NullSource;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import java.util.HashSet;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class ProfileStateTest {
ProfileState profileState;
Set<AlarmConditionFilterKey> entityKeys = new HashSet<>();
Set<AlarmConditionFilterKey> ruleKeys = new HashSet<>();
@BeforeEach
void setUp() {
profileState = mock(ProfileState.class);
}
@ParameterizedTest()
@EnumSource(DynamicValueSourceType.class)
@NullSource
void addScheduleDynamicValuesSourceAttribute(DynamicValueSourceType sourceType) {
willCallRealMethod().given(profileState).addScheduleDynamicValues(any(), any());
final DynamicValue<String> dynamicValue = new DynamicValue<>(sourceType, "myKey");
SpecificTimeSchedule schedule = new SpecificTimeSchedule();
schedule.setDynamicValue(dynamicValue);
Assertions.assertThat(entityKeys.isEmpty()).isTrue();
Assertions.assertThat(schedule.getDynamicValue().getSourceAttribute()).isNotNull();
profileState.addScheduleDynamicValues(schedule, entityKeys);
Assertions.assertThat(entityKeys).isEqualTo(Set.of(
new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "myKey")));
}
@ParameterizedTest()
@EnumSource(DynamicValueSourceType.class)
@NullSource
void addScheduleDynamicValuesSourceAttributeIsNull(DynamicValueSourceType sourceType) {
willCallRealMethod().given(profileState).addScheduleDynamicValues(any(), any());
DynamicValue<String> dynamicValue = new DynamicValue<>(sourceType, null);
SpecificTimeSchedule schedule = new SpecificTimeSchedule();
schedule.setDynamicValue(dynamicValue);
Assertions.assertThat(entityKeys.isEmpty()).isTrue();
Assertions.assertThat(schedule.getDynamicValue().getSourceAttribute()).isNull();
profileState.addScheduleDynamicValues(schedule, entityKeys);
Assertions.assertThat(entityKeys.isEmpty()).isTrue();
}
@ParameterizedTest()
@EnumSource(value = FilterPredicateType.class, names = {"COMPLEX"})
void addDynamicValuesRecursivelySourceAttributeComplexKeyFilter(FilterPredicateType predicateType) {
willCallRealMethod().given(profileState).addDynamicValuesRecursively(any(), any(), any());
ComplexFilterPredicate predicate = mock(ComplexFilterPredicate.class, RETURNS_DEEP_STUBS);
willReturn(predicateType).given(predicate).getType();
profileState.addDynamicValuesRecursively(predicate, entityKeys, ruleKeys);
Assertions.assertThat(entityKeys.isEmpty()).isTrue();
Assertions.assertThat(ruleKeys.isEmpty()).isTrue();
}
@ParameterizedTest()
@EnumSource(value = FilterPredicateType.class, names = {"STRING", "NUMERIC", "BOOLEAN"})
void addDynamicValuesRecursivelySourceAttributeIsNull(FilterPredicateType predicateType) {
willCallRealMethod().given(profileState).addDynamicValuesRecursively(any(), any(), any());
SimpleKeyFilterPredicate<String> predicate = mock(SimpleKeyFilterPredicate.class, RETURNS_DEEP_STUBS);
willReturn(predicateType).given(predicate).getType();
when(predicate.getValue().getDynamicValue().getSourceType()).thenReturn(DynamicValueSourceType.CURRENT_DEVICE);
when(predicate.getValue().getDynamicValue().getSourceAttribute()).thenReturn(null);
profileState.addDynamicValuesRecursively(predicate, entityKeys, ruleKeys);
Assertions.assertThat(entityKeys.isEmpty()).isTrue();
Assertions.assertThat(ruleKeys.isEmpty()).isTrue();
}
@ParameterizedTest()
@EnumSource(value = FilterPredicateType.class, names = {"STRING", "NUMERIC", "BOOLEAN"})
void addDynamicValuesRecursivelySourceAttributeAdded(FilterPredicateType predicateType) {
willCallRealMethod().given(profileState).addDynamicValuesRecursively(any(), any(), any());
SimpleKeyFilterPredicate<String> predicate = mock(SimpleKeyFilterPredicate.class, RETURNS_DEEP_STUBS);
willReturn(predicateType).given(predicate).getType();
when(predicate.getValue().getDynamicValue().getSourceType()).thenReturn(DynamicValueSourceType.CURRENT_DEVICE);
when(predicate.getValue().getDynamicValue().getSourceAttribute()).thenReturn("myKey");
profileState.addDynamicValuesRecursively(predicate, entityKeys, ruleKeys);
Assertions.assertThat(entityKeys).isEqualTo(Set.of(
new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "myKey")));
Assertions.assertThat(ruleKeys).isEqualTo(Set.of(
new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "myKey")));
}
}

22
ui-ngx/src/app/core/utils.ts

@ -137,6 +137,14 @@ export function isLiteralObject(value: any) {
return (!!value) && (value.constructor === Object);
}
export const isDate = (obj: any): boolean => {
return Object.prototype.toString.call(obj) === "[object Date]";
}
export const isFile = (obj: any): boolean => {
return Object.prototype.toString.call(obj) === "[object File]";
}
export const formatValue = (value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined => {
if (isDefinedAndNotNull(value) && isNumeric(value) &&
(isDefinedAndNotNull(dec) || isNotEmptyStr(units) || Number(value).toString() === value)) {
@ -180,7 +188,7 @@ export function deleteNullProperties(obj: any) {
delete obj[propName];
} else if (isObject(obj[propName])) {
deleteNullProperties(obj[propName]);
} else if (obj[propName] instanceof Array) {
} else if (Array.isArray(obj[propName])) {
(obj[propName] as any[]).forEach((elem) => {
deleteNullProperties(elem);
});
@ -335,13 +343,11 @@ export function deepClone<T>(target: T, ignoreFields?: string[]): T {
if (isObservable(target)) {
return target;
}
if (target instanceof Date) {
return new Date(target.getTime()) as any;
if (isDate(target)) {
return new Date((target as Date).getTime()) as T;
}
if (target instanceof Array) {
const cp = [] as any[];
(target as any[]).forEach((v) => { cp.push(v); });
return cp.map((n: any) => deepClone<any>(n)) as any;
if (Array.isArray(target)) {
return (target as any[]).map((item) => deepClone(item)) as any;
}
if (typeof target === 'object') {
const cp = {...(target as { [key: string]: any })} as { [key: string]: any };
@ -752,7 +758,7 @@ export function sortObjectKeys<T>(obj: T): T {
}
export function deepTrim<T>(obj: T): T {
if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null || obj instanceof File) {
if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null || isFile(obj)) {
return obj;
}
return Object.keys(obj).reduce((acc, curr) => {

4
ui-ngx/src/app/modules/common/modules-map.ts

@ -76,8 +76,6 @@ import * as TranslateCore from '@ngx-translate/core';
import * as MatDateTimePicker from '@mat-datetimepicker/core';
import _moment from 'moment';
import * as tslib from 'tslib';
import * as Echarts from 'echarts';
import * as EchartsCore from 'echarts/core';
import * as TbCore from '@core/public-api';
import * as TbShared from '@shared/public-api';
@ -407,8 +405,6 @@ class ModulesMap implements IModulesMap {
'@mat-datetimepicker/core': MatDateTimePicker,
moment: _moment,
tslib,
'echarts': Echarts,
'echarts/core': EchartsCore,
'@core/public-api': TbCore,
'@shared/public-api': TbShared,

27
ui-ngx/src/app/modules/home/components/rule-node/filter/check-relation-config.component.ts

@ -54,24 +54,29 @@ export class CheckRelationConfigComponent extends RuleNodeConfigurationComponent
this.checkRelationConfigForm = this.fb.group({
checkForSingleEntity: [configuration.checkForSingleEntity, []],
direction: [configuration.direction, []],
entityType: [configuration.entityType,
configuration && configuration.checkForSingleEntity ? [Validators.required] : []],
entityId: [configuration.entityId,
configuration && configuration.checkForSingleEntity ? [Validators.required] : []],
entityType: [configuration.entityType, [Validators.required]],
entityId: [configuration.entityId, [Validators.required]],
relationType: [configuration.relationType, [Validators.required]]
});
}
protected validatorTriggers(): string[] {
return ['checkForSingleEntity'];
return ['checkForSingleEntity', 'entityType'];
}
protected updateValidators(emitEvent: boolean) {
const checkForSingleEntity: boolean = this.checkRelationConfigForm.get('checkForSingleEntity').value;
this.checkRelationConfigForm.get('entityType').setValidators(checkForSingleEntity ? [Validators.required] : []);
this.checkRelationConfigForm.get('entityType').updateValueAndValidity({emitEvent});
this.checkRelationConfigForm.get('entityId').setValidators(checkForSingleEntity ? [Validators.required] : []);
this.checkRelationConfigForm.get('entityId').updateValueAndValidity({emitEvent});
protected updateValidators(_emitEvent: boolean, trigger: string) {
if (trigger === 'entityType') {
this.checkRelationConfigForm.get('entityId').reset(null);
} else {
const checkForSingleEntity: boolean = this.checkRelationConfigForm.get('checkForSingleEntity').value;
if (checkForSingleEntity) {
this.checkRelationConfigForm.get('entityType').enable({emitEvent: false});
this.checkRelationConfigForm.get('entityId').enable({emitEvent: false});
} else {
this.checkRelationConfigForm.get('entityType').disable({emitEvent: false});
this.checkRelationConfigForm.get('entityId').disable({emitEvent: false});
}
}
}
}

1
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss

@ -33,6 +33,7 @@
.table-container {
overflow: auto;
z-index: 1;
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -271,7 +271,7 @@ export const updateScadaSymbolMetadataInContent = (svgContent: string, metadata:
const svgDoc = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
const parsererror = svgDoc.getElementsByTagName('parsererror');
if (parsererror?.length) {
return parsererror[0].outerHTML;
throw Error(parsererror[0].textContent)
}
updateScadaSymbolMetadataInDom(svgDoc, metadata);
return svgDoc.documentElement.outerHTML;

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

Loading…
Cancel
Save