Browse Source

Merge branch 'rc' of github.com:thingsboard/thingsboard into performance

pull/13510/head
IrynaMatveieva 1 year ago
parent
commit
944dce2fe5
  1. 4
      README.md
  2. 6
      application/pom.xml
  3. 2
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  4. 2
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  5. 2
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  6. 2
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  7. 5
      application/src/main/data/json/system/widget_types/knob_control.json
  8. 45
      application/src/main/data/upgrade/basic/schema_update.sql
  9. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  10. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  11. 124
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  12. 3
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  13. 7
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  14. 117
      application/src/main/java/org/thingsboard/server/controller/JobController.java
  15. 48
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  16. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  17. 2
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  19. 13
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  20. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  21. 26
      application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java
  22. 155
      application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java
  24. 32
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  25. 1
      application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
  26. 43
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/JobsDeletionTaskProcessor.java
  27. 46
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  28. 207
      application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java
  29. 94
      application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java
  30. 36
      application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java
  31. 115
      application/src/main/java/org/thingsboard/server/service/job/JobStatsProcessor.java
  32. 59
      application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java
  33. 50
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java
  34. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  35. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  36. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  37. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  38. 10
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
  39. 1
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  40. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  41. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  42. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  43. 4
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  44. 12
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java
  45. 6
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  46. 71
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
  47. 30
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  48. 92
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTbTelemetryService.java
  49. 43
      application/src/main/java/org/thingsboard/server/service/telemetry/TbTelemetryService.java
  50. 2
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  51. 50
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java
  52. 30
      application/src/main/resources/thingsboard.yml
  53. 33
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  54. 81
      application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
  55. 7
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  56. 38
      application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
  57. 8
      application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java
  58. 9
      application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
  59. 9
      application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java
  60. 504
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java
  61. 43
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java
  62. 23
      application/src/test/java/org/thingsboard/server/service/job/TestTaskProcessor.java
  63. 19
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  64. 4
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  65. 65
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  66. 2
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java
  67. 79
      application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java
  68. 130
      application/src/test/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtxTest.java
  69. 2
      common/actor/pom.xml
  70. 2
      common/cache/pom.xml
  71. 2
      common/cluster-api/pom.xml
  72. 2
      common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java
  73. 2
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueCallback.java
  74. 3
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueProducer.java
  75. 6
      common/coap-server/pom.xml
  76. 2
      common/dao-api/pom.xml
  77. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
  78. 48
      common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java
  79. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
  80. 2
      common/data/pom.xml
  81. 20
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  82. 6
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/AttributeKv.java
  83. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObject.java
  84. 18
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObjectKey.java
  85. 75
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java
  86. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/Entity.java
  87. 7
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/LatestTsKv.java
  88. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsMsg.java
  89. 6
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/ToCoreEdqsRequest.java
  90. 13
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/AbstractEntityFields.java
  91. 5
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityFields.java
  92. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
  93. 3
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java
  94. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  95. 38
      common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java
  96. 51
      common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java
  97. 25
      common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java
  98. 85
      common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java
  99. 45
      common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java
  100. 23
      common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java

4
README.md

@ -5,6 +5,7 @@ ThingsBoard is an open-source IoT platform for data collection, processing, visu
<img src="./img/logo.png?raw=true" width="100" height="100">
## Documentation
ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs).
@ -14,6 +15,9 @@ ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/d
[**Smart energy**](https://thingsboard.io/smart-energy/)
[![Smart energy](https://user-images.githubusercontent.com/8308069/152984256-eb48564a-645c-468d-912b-f554b63104a5.gif "Smart energy")](https://thingsboard.io/smart-energy/)
[**SCADA Swimming pool**](https://thingsboard.io/use-cases/scada/)
[![SCADA Swimming pool](https://github.com/user-attachments/assets/0878a2f5-d358-47c5-b295-03b4533685cf "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/)
[**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
[![Fleet tracking](https://user-images.githubusercontent.com/8308069/152984528-0054ed55-8b8b-4cda-ba45-02fe95a81222.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)

6
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>
@ -128,6 +128,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>edqs</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>dao</artifactId>

2
application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg

@ -7,7 +7,7 @@
"tags": [
{
"tag": "arrow",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n element.stroke(ctx.properties.lineColor).fill(ctx.properties.lineColor);\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

2
application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg

@ -7,7 +7,7 @@
"tags": [
{
"tag": "arrow",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n element.stroke(ctx.properties.lineColor).fill(ctx.properties.lineColor);\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

2
application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg

@ -7,7 +7,7 @@
"tags": [
{
"tag": "arrow",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n element.stroke(ctx.properties.lineColor).fill(ctx.properties.lineColor);\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

2
application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg

@ -7,7 +7,7 @@
"tags": [
{
"tag": "arrow",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"stateRenderFunction": "var arrow = ctx.values.arrow;\nif (arrow) {\n element.show();\n element.stroke(ctx.properties.lineColor).fill(ctx.properties.lineColor);\n var arrowDirection = ctx.values.arrowDirection;\n var direticon = element.remember('direticon');\n var angle = arrowDirection ? 0 : 180;\n if (!direticon) {\n element.transform({rotate: angle});\n } else {\n ctx.api.transform({rotate: angle, originY: 100});\n element.remember('direticon', false);\n }\n} else {\n element.hide();\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

5
application/src/main/data/json/system/widget_types/knob_control.json

@ -12,10 +12,9 @@
"templateHtml": "<tb-knob [ctx]='ctx'></tb-knob>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"dataKeySettingsForm": [],
"settingsDirective": "tb-knob-control-widget-settings",
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\",\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}"
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2,\"units\":null}"
},
"tags": [
"dial",

45
application/src/main/data/upgrade/basic/schema_update.sql

@ -14,3 +14,48 @@
-- limitations under the License.
--
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START
UPDATE tenant_profile
SET profile_data = jsonb_set(
profile_data,
'{configuration}',
(
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration'
||
COALESCE(
CASE
WHEN profile_data -> 'configuration' ->
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN
jsonb_build_object(
'cassandraReadQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraReadQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration'
)
END,
'{}'::jsonb
)
)
)
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration';
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START
UPDATE notification_rule
SET trigger_config = REGEXP_REPLACE(
trigger_config,
'"CASSANDRA_QUERIES"',
'"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"',
'g'
)
WHERE trigger_type = 'RATE_LIMITS'
AND trigger_config LIKE '%"CASSANDRA_QUERIES"%';
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END

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

@ -31,6 +31,7 @@ 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.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
@ -96,6 +97,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeStateService;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -548,6 +550,14 @@ public class ActorSystemContext {
@Getter
private CalculatedFieldQueueService calculatedFieldQueueService;
@Autowired
@Getter
private JobService jobService;
@Autowired
@Getter
private JobManager jobManager;
@Value("${actors.session.max_concurrent_sessions_per_device:1}")
@Getter
private int maxConcurrentSessionsPerDevice;

12
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -24,6 +24,7 @@ import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
@ -93,6 +94,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
@ -889,6 +891,16 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getCalculatedFieldQueueService();
}
@Override
public JobService getJobService() {
return mainCtx.getJobService();
}
@Override
public JobManager getJobManager() {
return mainCtx.getJobManager();
}
@Override
public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck();

124
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -23,8 +23,6 @@ import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation;
import lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -92,6 +90,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
@ -108,6 +107,7 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
@ -146,6 +146,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.model.ModelConstants;
@ -370,6 +371,9 @@ public abstract class BaseController {
@Autowired
protected NotificationTargetService notificationTargetService;
@Autowired
protected JobService jobService;
@Autowired
protected CalculatedFieldService calculatedFieldService;
@ -385,7 +389,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause();
} else {
e = thingsboardException;
@ -430,7 +434,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
@ -595,88 +599,39 @@ public abstract class BaseController {
}
}
protected void checkEntityId(EntityId entityId, Operation operation) throws ThingsboardException {
protected HasId<? extends EntityId> checkEntityId(EntityId entityId, Operation operation) throws ThingsboardException {
try {
if (entityId == null) {
throw new ThingsboardException("Parameter entityId can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
validateId(entityId.getId(), id -> "Incorrect entityId " + id);
switch (entityId.getEntityType()) {
case ALARM:
checkAlarmId(new AlarmId(entityId.getId()), operation);
return;
case DEVICE:
checkDeviceId(new DeviceId(entityId.getId()), operation);
return;
case DEVICE_PROFILE:
checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation);
return;
case CUSTOMER:
checkCustomerId(new CustomerId(entityId.getId()), operation);
return;
case TENANT:
checkTenantId(TenantId.fromUUID(entityId.getId()), operation);
return;
case TENANT_PROFILE:
checkTenantProfileId(new TenantProfileId(entityId.getId()), operation);
return;
case RULE_CHAIN:
checkRuleChain(new RuleChainId(entityId.getId()), operation);
return;
case RULE_NODE:
checkRuleNode(new RuleNodeId(entityId.getId()), operation);
return;
case ASSET:
checkAssetId(new AssetId(entityId.getId()), operation);
return;
case ASSET_PROFILE:
checkAssetProfileId(new AssetProfileId(entityId.getId()), operation);
return;
case DASHBOARD:
checkDashboardId(new DashboardId(entityId.getId()), operation);
return;
case USER:
checkUserId(new UserId(entityId.getId()), operation);
return;
case ENTITY_VIEW:
checkEntityViewId(new EntityViewId(entityId.getId()), operation);
return;
case EDGE:
checkEdgeId(new EdgeId(entityId.getId()), operation);
return;
case WIDGETS_BUNDLE:
checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation);
return;
case WIDGET_TYPE:
checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation);
return;
case TB_RESOURCE:
checkResourceInfoId(new TbResourceId(entityId.getId()), operation);
return;
case OTA_PACKAGE:
checkOtaPackageId(new OtaPackageId(entityId.getId()), operation);
return;
case QUEUE:
checkQueueId(new QueueId(entityId.getId()), operation);
return;
case OAUTH2_CLIENT:
checkOauth2ClientId(new OAuth2ClientId(entityId.getId()), operation);
return;
case DOMAIN:
checkDomainId(new DomainId(entityId.getId()), operation);
return;
case MOBILE_APP:
checkMobileAppId(new MobileAppId(entityId.getId()), operation);
return;
case MOBILE_APP_BUNDLE:
checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
return;
case CALCULATED_FIELD:
checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
return;
default:
checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}
return switch (entityId.getEntityType()) {
case ALARM -> checkAlarmId(new AlarmId(entityId.getId()), operation);
case DEVICE -> checkDeviceId(new DeviceId(entityId.getId()), operation);
case DEVICE_PROFILE -> checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation);
case CUSTOMER -> checkCustomerId(new CustomerId(entityId.getId()), operation);
case TENANT -> checkTenantId(TenantId.fromUUID(entityId.getId()), operation);
case TENANT_PROFILE -> checkTenantProfileId(new TenantProfileId(entityId.getId()), operation);
case RULE_CHAIN -> checkRuleChain(new RuleChainId(entityId.getId()), operation);
case RULE_NODE -> checkRuleNode(new RuleNodeId(entityId.getId()), operation);
case ASSET -> checkAssetId(new AssetId(entityId.getId()), operation);
case ASSET_PROFILE -> checkAssetProfileId(new AssetProfileId(entityId.getId()), operation);
case DASHBOARD -> checkDashboardId(new DashboardId(entityId.getId()), operation);
case USER -> checkUserId(new UserId(entityId.getId()), operation);
case ENTITY_VIEW -> checkEntityViewId(new EntityViewId(entityId.getId()), operation);
case EDGE -> checkEdgeId(new EdgeId(entityId.getId()), operation);
case WIDGETS_BUNDLE -> checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation);
case WIDGET_TYPE -> checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation);
case TB_RESOURCE -> checkResourceInfoId(new TbResourceId(entityId.getId()), operation);
case OTA_PACKAGE -> checkOtaPackageId(new OtaPackageId(entityId.getId()), operation);
case QUEUE -> checkQueueId(new QueueId(entityId.getId()), operation);
case OAUTH2_CLIENT -> checkOauth2ClientId(new OAuth2ClientId(entityId.getId()), operation);
case DOMAIN -> checkDomainId(new DomainId(entityId.getId()), operation);
case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation);
case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
};
} catch (Exception e) {
throw handleException(e, false);
}
@ -874,6 +829,10 @@ public abstract class BaseController {
return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, operation);
}
Job checkJobId(JobId jobId, Operation operation) throws ThingsboardException {
return checkEntityId(jobId, jobService::findJobById, operation);
}
protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
@ -957,12 +916,13 @@ public abstract class BaseController {
}
}
private void checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException {
private CalculatedField checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException {
validateId(calculatedFieldId, "Invalid entity id");
SecurityUser user = getCurrentUser();
CalculatedField cf = calculatedFieldService.findById(user.getTenantId(), calculatedFieldId);
checkNotNull(cf, calculatedFieldId.getEntityType().getNormalName() + " with id [" + calculatedFieldId + "] is not found");
checkEntityId(cf.getEntityId(), operation);
return cf;
}
protected HomeDashboardInfo getHomeDashboardInfo(SecurityUser securityUser, JsonNode additionalInfo) {

3
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -98,7 +98,8 @@ public class CalculatedFieldController extends BaseController {
public static final int TIMEOUT = 20;
private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n"
private static final String TEST_SCRIPT_EXPRESSION =
"Execute the Script expression and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ "{\n" +
" \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" +

7
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
@ -149,9 +150,9 @@ public class EntityQueryController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@GetMapping("/edqs/enabled")
public boolean isEdqsApiEnabled() {
return edqsApiService.isEnabled();
@GetMapping("/edqs/state")
public EdqsState getEdqsState() {
return edqsService.getState();
}
}

117
application/src/main/java/org/thingsboard/server/controller/JobController.java

@ -0,0 +1,117 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobFilter;
import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class JobController extends BaseController {
private final JobManager jobManager;
@GetMapping("/job/{id}")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public Job getJobById(@PathVariable UUID id) throws ThingsboardException {
JobId jobId = new JobId(id);
return checkJobId(jobId, Operation.READ);
}
@GetMapping("/jobs")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public PageData<Job> getJobs(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on job's description")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@RequestParam(required = false) List<JobType> types,
@RequestParam(required = false) List<JobStatus> statuses,
@RequestParam(required = false) List<UUID> entities,
@RequestParam(required = false) Long startTime,
@RequestParam(required = false) Long endTime) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
JobFilter filter = JobFilter.builder()
.types(types)
.statuses(statuses)
.entities(entities)
.startTime(startTime)
.endTime(endTime)
.build();
return jobService.findJobsByFilter(getTenantId(), filter, pageLink);
}
@PostMapping("/job/{id}/cancel")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void cancelJob(@PathVariable UUID id) throws ThingsboardException {
JobId jobId = new JobId(id);
checkJobId(jobId, Operation.WRITE);
jobManager.cancelJob(getTenantId(), jobId);
}
@PostMapping("/job/{id}/reprocess")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void reprocessJob(@PathVariable UUID id) throws ThingsboardException {
JobId jobId = new JobId(id);
checkJobId(jobId, Operation.WRITE);
jobManager.reprocessJob(getTenantId(), jobId);
}
@DeleteMapping("/job/{id}")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void deleteJob(@PathVariable UUID id) throws ThingsboardException {
JobId jobId = new JobId(id);
checkJobId(jobId, Operation.DELETE);
jobService.deleteJob(getTenantId(), jobId);
}
}

48
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -64,11 +64,9 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AggregationParams;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
@ -78,7 +76,6 @@ import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -92,6 +89,7 @@ import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.AttributeData;
import org.thingsboard.server.service.telemetry.TbTelemetryService;
import org.thingsboard.server.service.telemetry.TsData;
import java.util.ArrayList;
@ -155,6 +153,9 @@ public class TelemetryController extends BaseController {
@Autowired
private AccessValidator accessValidator;
@Autowired
private TbTelemetryService tbTelemetryService;
@Value("${transport.json.max_string_value_length:0}")
private int maxStringValueLength;
@ -313,30 +314,21 @@ public class TelemetryController extends BaseController {
@Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.")
@RequestParam(name = "timeZone", required = false) String timeZone,
@Parameter(description = "An integer value that represents a max number of time series data points to fetch." +
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100"))
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100"))
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@Parameter(description = "A string value representing the aggregation function. " +
"If the interval is not specified, 'agg' parameter will use 'NONE' value.",
"If the interval is not specified, 'agg' parameter will use 'NONE' value.",
schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"}))
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> {
AggregationParams params;
Aggregation agg = Aggregation.valueOf(aggStr);
if (Aggregation.NONE.equals(agg)) {
params = AggregationParams.none();
} else if (intervalType == null || IntervalType.MILLISECONDS.equals(intervalType)) {
params = interval == 0L ? AggregationParams.none() : AggregationParams.milliseconds(agg, interval);
} else {
params = AggregationParams.calendar(agg, intervalType, timeZone);
}
List<ReadTsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, params, limit, orderBy)).collect(Collectors.toList());
Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());
});
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
}
@ApiOperation(value = "Save device attributes (saveDeviceAttributes)",
@ -345,12 +337,12 @@ public class TelemetryController extends BaseController {
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."),
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."),
@ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST),
@ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."),
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
@ -468,11 +460,11 @@ public class TelemetryController extends BaseController {
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
@ -549,11 +541,11 @@ public class TelemetryController extends BaseController {
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
@ -571,11 +563,11 @@ public class TelemetryController extends BaseController {
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -116,6 +116,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes();
// TODO: cleanup update code after each release
systemDataLoaderService.updateDefaultNotificationConfigs(false);
// Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData();

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

@ -72,7 +72,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
@Override
public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) {
stateService.update(queueKey, partitions);
stateService.update(queueKey, partitions, null);
}
@Override

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

@ -71,7 +71,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
.queueKey(queueKey)
.topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval)
.msgPackProcessor((msgs, consumer, config) -> {
.msgPackProcessor((msgs, consumer, consumerKey, config) -> {
for (TbProtoQueueMsg<CalculatedFieldStateProto> msg : msgs) {
try {
if (msg.getValue() != null) {

13
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -16,13 +16,16 @@
package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.util.ProtoUtils;
@ -90,7 +93,15 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
@Override
public TbelCfArg toTbelCfArg() {
return new TbelCfSingleValueArg(ts, kvEntryValue.getValue());
Object value = kvEntryValue.getValue();
if (kvEntryValue instanceof JsonDataEntry) {
try {
value = JacksonUtil.readValue(kvEntryValue.getValueAsString(), new TypeReference<>() {
});
} catch (Exception e) {
}
}
return new TbelCfSingleValueArg(ts, value);
}
@Override

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -22,7 +22,6 @@ import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.stub.StreamObserver;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -63,6 +62,7 @@ import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.kafka.TbKafkaSettings;
import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@ -162,8 +162,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private ScheduledExecutorService executorService;
@PostConstruct
public void init() {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void onStartUp() {
log.info("Initializing Edge RPC service!");
NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort)
.permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS)

26
application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsApiService.java

@ -22,7 +22,6 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
@ -51,11 +50,6 @@ public class DefaultEdqsApiService implements EdqsApiService {
private final EdqsClientQueueFactory queueFactory;
private TbQueueRequestTemplate<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>> requestTemplate;
@Value("${queue.edqs.api.auto_enable:true}")
private boolean autoEnable;
private Boolean apiEnabled = null;
@PostConstruct
private void init() {
requestTemplate = queueFactory.createEdqsRequestTemplate();
@ -85,31 +79,11 @@ public class DefaultEdqsApiService implements EdqsApiService {
}, MoreExecutors.directExecutor());
}
@Override
public boolean isEnabled() {
return Boolean.TRUE.equals(apiEnabled);
}
@Override
public void setEnabled(boolean enabled) {
if (enabled) {
log.info("Enabling EDQS API");
} else {
log.info("Disabling EDQS API");
}
apiEnabled = enabled;
}
@Override
public boolean isSupported() {
return true;
}
@Override
public boolean isAutoEnable() {
return autoEnable;
}
@PreDestroy
private void stop() {
requestTemplate.stop();

155
application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java

@ -20,11 +20,13 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -36,6 +38,9 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.edqs.EdqsEventType;
import org.thingsboard.server.common.data.edqs.EdqsObject;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsSyncStatus;
import org.thingsboard.server.common.data.edqs.EdqsSyncRequest;
import org.thingsboard.server.common.data.edqs.Entity;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsMsg;
@ -51,20 +56,25 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.edqs.processor.EdqsProducer;
import org.thingsboard.server.edqs.state.EdqsPartitionService;
import org.thingsboard.server.edqs.util.EdqsConverter;
import org.thingsboard.server.edqs.util.DefaultEdqsMapper;
import org.thingsboard.server.edqs.util.EdqsMapper;
import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsCoreServiceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.HashPartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.environment.DistributedLock;
import org.thingsboard.server.queue.environment.DistributedLockService;
import org.thingsboard.server.queue.provider.EdqsClientQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
@ -74,31 +84,42 @@ import java.util.concurrent.TimeUnit;
public class DefaultEdqsService implements EdqsService {
private final EdqsClientQueueFactory queueFactory;
private final EdqsConverter edqsConverter;
private final EdqsMapper edqsMapper;
private final EdqsSyncService edqsSyncService;
private final EdqsApiService edqsApiService;
private final DistributedLockService distributedLockService;
private final AttributesService attributesService;
private final EdqsPartitionService edqsPartitionService;
private final TopicService topicService;
private final TbServiceInfoProvider serviceInfoProvider;
private final DiscoveryService discoveryService;
@Autowired @Lazy
private TbClusterService clusterService;
@Autowired @Lazy
private HashPartitionService hashPartitionService;
@Value("${queue.edqs.api.auto_enable:true}")
private boolean autoEnableApi;
@Value("${queue.edqs.readiness_check_interval:60000}")
private int edqsReadinessCheckInterval;
private EdqsProducer eventsProducer;
private ExecutorService executor;
private ScheduledExecutorService scheduler;
private DistributedLock syncLock;
@Getter
private EdqsState state;
@PostConstruct
private void init() {
executor = ThingsBoardExecutors.newWorkStealingPool(12, getClass());
scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-check");
eventsProducer = EdqsProducer.builder()
.producer(queueFactory.createEdqsEventsProducer())
.partitionService(edqsPartitionService)
.build();
syncLock = distributedLockService.getLock("edqs_sync");
state = new EdqsState();
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
@ -106,6 +127,26 @@ public class DefaultEdqsService implements EdqsService {
if (!serviceInfoProvider.isService(ServiceType.TB_CORE)) {
return;
}
if (edqsApiService.isSupported()) {
scheduler.scheduleWithFixedDelay(() -> {
if (!hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) {
return;
}
List<ServiceInfo> servers = new ArrayList<>(discoveryService.getOtherServers());
servers.add(serviceInfoProvider.getServiceInfo());
List<ServiceInfo> readyEdqsServers = servers.stream()
.filter(serviceInfo -> serviceInfo.getServiceTypesList().contains(ServiceType.EDQS.name()))
.filter(ServiceInfo::getReady)
.toList();
boolean changed = state.updateEdqsReady(!readyEdqsServers.isEmpty());
if (changed) {
broadcastEdqsReady(state.getEdqsReady());
}
}, 0, edqsReadinessCheckInterval, TimeUnit.MILLISECONDS);
}
executor.submit(() -> {
try {
EdqsSyncState syncState = getSyncState();
@ -115,9 +156,9 @@ public class DefaultEdqsService implements EdqsService {
.syncRequest(new EdqsSyncRequest())
.build());
}
} else if (edqsApiService.isSupported() && edqsApiService.isAutoEnable()) {
} else {
// only if topic/RocksDB is not empty and sync is finished
edqsApiService.setEnabled(true);
onSyncStatusUpdate(EdqsSyncStatus.FINISHED);
}
} catch (Throwable e) {
log.error("Failed to start EDQS service", e);
@ -131,7 +172,10 @@ public class DefaultEdqsService implements EdqsService {
if (request.getSyncRequest() != null) {
saveSyncState(EdqsSyncStatus.REQUESTED);
}
broadcast(request.toInternalMsg());
broadcast(ToCoreEdqsMsg.builder()
.syncRequest(request.getSyncRequest())
.apiEnabled(request.getApiEnabled())
.build());
}
@Override
@ -140,7 +184,18 @@ public class DefaultEdqsService implements EdqsService {
log.info("Processing system msg {}", msg);
try {
if (msg.getApiEnabled() != null) {
edqsApiService.setEnabled(msg.getApiEnabled());
if (msg.getApiEnabled()) {
state.setApiMode(EdqsApiMode.ENABLED);
} else {
state.setApiMode(EdqsApiMode.DISABLED);
}
log.info("New state: {}", state);
}
if (msg.getEdqsReady() != null) {
onEdqsReady(msg.getEdqsReady());
}
if (msg.getSyncStatus() != null) {
onSyncStatusUpdate(msg.getSyncStatus());
}
if (msg.getSyncRequest() != null) {
@ -154,23 +209,16 @@ public class DefaultEdqsService implements EdqsService {
return;
}
}
saveSyncState(EdqsSyncStatus.STARTED);
edqsSyncService.sync();
saveSyncState(EdqsSyncStatus.FINISHED);
if (edqsApiService.isSupported())
if (edqsApiService.isAutoEnable()) {
log.info("EDQS sync is finished, auto-enabling API");
broadcast(ToCoreEdqsMsg.builder()
.apiEnabled(Boolean.TRUE)
.build());
} else {
log.info("EDQS sync is finished, but leaving API disabled");
}
saveSyncState(EdqsSyncStatus.FINISHED);
broadcastSyncStatusUpdate(EdqsSyncStatus.FINISHED);
} catch (Exception e) {
log.error("Failed to complete sync", e);
saveSyncState(EdqsSyncStatus.FAILED);
broadcastSyncStatusUpdate(EdqsSyncStatus.FAILED);
} finally {
syncLock.unlock();
}
@ -181,6 +229,60 @@ public class DefaultEdqsService implements EdqsService {
});
}
private void broadcastEdqsReady(boolean ready) {
broadcast(ToCoreEdqsMsg.builder()
.edqsReady(ready)
.build());
}
private void onEdqsReady(boolean ready) {
state.updateEdqsReady(ready);
checkState();
}
private void broadcastSyncStatusUpdate(EdqsSyncStatus status) {
broadcast(ToCoreEdqsMsg.builder()
.syncStatus(status)
.build());
}
private void onSyncStatusUpdate(EdqsSyncStatus status) {
state.setSyncStatus(status);
checkState();
}
private void checkState() {
if (!edqsApiService.isSupported()) {
log.info("New state: {}. EDQS API not supported", state);
return;
}
if (state.isApiReady()) {
if (autoEnableApi) {
if (state.getApiMode() == null || state.getApiMode() == EdqsApiMode.AUTO_DISABLED) {
state.setApiMode(EdqsApiMode.AUTO_ENABLED);
log.info("New state: {}. Auto-enabled EDQS API", state);
} else {
log.info("New state: {}. API mode left as is", state);
}
} else {
log.info("New state: {}. API auto-enabling is disabled", state);
}
} else {
if (state.isApiEnabled()) {
state.setApiMode(EdqsApiMode.AUTO_DISABLED);
log.info("New state: {}. Disabled EDQS API", state);
} else {
log.info("New state: {}. API left disabled", state);
}
}
}
@Override
public boolean isApiEnabled() {
return state.isApiEnabled();
}
@Override
public void onUpdate(TenantId tenantId, EntityId entityId, Object entity) {
EntityType entityType = entityId.getEntityType();
@ -189,7 +291,7 @@ public class DefaultEdqsService implements EdqsService {
log.trace("[{}][{}] Ignoring update event, type {} not supported", tenantId, entityId, entityType);
return;
}
onUpdate(tenantId, objectType, edqsConverter.toEntity(entityType, entity));
onUpdate(tenantId, objectType, DefaultEdqsMapper.toEntity(entityType, entity));
}
@Override
@ -216,12 +318,11 @@ public class DefaultEdqsService implements EdqsService {
protected void processEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType, EdqsObject object) {
executor.submit(() -> {
try {
String key = object.key();
String key = object.stringKey();
Long version = object.version();
EdqsEventMsg.Builder eventMsg = EdqsEventMsg.newBuilder()
.setKey(key)
.setObjectType(objectType.name())
.setData(ByteString.copyFrom(edqsConverter.serialize(objectType, object)))
.setData(ByteString.copyFrom(edqsMapper.serialize(object)))
.setEventType(eventType.name());
if (version != null) {
eventMsg.setVersion(version);
@ -278,6 +379,7 @@ public class DefaultEdqsService implements EdqsService {
@PreDestroy
private void stop() {
executor.shutdown();
scheduler.shutdownNow();
eventsProducer.stop();
}
@ -288,11 +390,4 @@ public class DefaultEdqsService implements EdqsService {
private EdqsSyncStatus status;
}
private enum EdqsSyncStatus {
REQUESTED,
STARTED,
FINISHED,
FAILED
}
}

2
application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java

@ -95,7 +95,7 @@ public abstract class EdqsSyncService {
syncLatestTimeseries();
counters.clear();
log.info("Finishing synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs));
log.info("Finished synchronizing data to EDQS in {} ms", (System.currentTimeMillis() - startTs));
}
private void process(TenantId tenantId, ObjectType type, EdqsObject object) {

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

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -58,6 +59,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.rule.engine.api.JobManager;
import java.util.Set;
@ -69,6 +71,7 @@ public class EntityStateSourcingListener {
private final TenantService tenantService;
private final TbClusterService tbClusterService;
private final EdgeSynchronizationManager edgeSynchronizationManager;
private final JobManager jobManager;
@PostConstruct
public void init() {
@ -134,6 +137,9 @@ public class EntityStateSourcingListener {
case CALCULATED_FIELD -> {
onCalculatedFieldUpdate(event.getEntity(), event.getOldEntity());
}
case JOB -> {
onJobUpdate((Job) event.getEntity());
}
default -> {
}
}
@ -212,8 +218,8 @@ public class EntityStateSourcingListener {
public void handleEvent(ActionEntityEvent<?> event) {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
if (ActionType.CREDENTIALS_UPDATED.equals(event.getActionType()) &&
EntityType.DEVICE.equals(event.getEntityId().getEntityType())
&& event.getEntity() instanceof DeviceCredentials) {
EntityType.DEVICE.equals(event.getEntityId().getEntityType())
&& event.getEntity() instanceof DeviceCredentials) {
tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(),
(DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null);
} else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) {
@ -295,6 +301,28 @@ public class EntityStateSourcingListener {
tbClusterService.onCalculatedFieldUpdated(calculatedField, oldCalculatedField, TbQueueCallback.EMPTY);
}
private void onJobUpdate(Job job) {
jobManager.onJobUpdate(job);
ComponentLifecycleEvent event;
if (job.getResult().getCancellationTs() > 0) {
event = ComponentLifecycleEvent.STOPPED;
} else if (job.getResult().getGeneralError() != null) {
event = ComponentLifecycleEvent.FAILED;
} else {
return;
}
ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder()
.tenantId(job.getTenantId())
.entityId(job.getId())
.event(event)
.info(JacksonUtil.newObjectNode()
.put("tasksKey", job.getConfiguration().getTasksKey()))
.build();
// task processors will add this job to the list of discarded
tbClusterService.broadcast(msg);
}
private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) {
String data = JacksonUtil.toString(JacksonUtil.valueToTree(assignedDevice));
if (data != null) {

1
application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java

@ -165,6 +165,7 @@ public class HousekeeperService {
private void stop() {
consumer.stop();
consumerExecutor.shutdownNow();
taskExecutor.shutdownNow();
log.info("Stopped Housekeeper service");
}

43
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/JobsDeletionTaskProcessor.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.housekeeper.processor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
import org.thingsboard.server.dao.job.JobService;
@Component
@RequiredArgsConstructor
@Slf4j
public class JobsDeletionTaskProcessor extends HousekeeperTaskProcessor<HousekeeperTask> {
private final JobService jobService;
@Override
public void process(HousekeeperTask task) throws Exception {
int deletedCount = jobService.deleteJobsByEntityId(task.getTenantId(), task.getEntityId());
log.debug("[{}][{}][{}] Deleted {} jobs", task.getTenantId(), task.getEntityId().getEntityType(), task.getEntityId(), deletedCount);
}
@Override
public HousekeeperTaskType getTaskType() {
return HousekeeperTaskType.DELETE_JOBS;
}
}

46
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -34,8 +35,10 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DbUpgradeExecutorService;
@ -69,14 +72,57 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private DbUpgradeExecutorService executorService;
@Autowired
private TenantProfileService tenantProfileService;
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
//TODO: should be cleaned after each release
updateInputNodes();
deduplicateRateLimitsPerSecondsConfigurations();
log.info("Data updated.");
}
private void deduplicateRateLimitsPerSecondsConfigurations() {
log.info("Starting update of tenant profiles...");
int totalProfiles = 0;
int updatedTenantProfiles = 0;
int skippedProfiles = 0;
int failedProfiles = 0;
var tenantProfiles = new PageDataIterable<>(
pageLink -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink), 1024);
for (TenantProfile tenantProfile : tenantProfiles) {
totalProfiles++;
String profileName = tenantProfile.getName();
UUID profileId = tenantProfile.getId().getId();
try {
Optional<DefaultTenantProfileConfiguration> profileConfiguration = tenantProfile.getProfileConfiguration();
if (profileConfiguration.isEmpty()) {
log.debug("[{}][{}] Skipping tenant profile with non-default configuration.", profileId, profileName);
skippedProfiles++;
continue;
}
DefaultTenantProfileConfiguration defaultTenantProfileConfiguration = profileConfiguration.get();
defaultTenantProfileConfiguration.deduplicateRateLimitsConfigs();
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
updatedTenantProfiles++;
log.debug("[{}][{}] Successfully updated tenant profile.", profileId, profileName);
} catch (Exception e) {
log.error("[{}][{}] Failed to updated tenant profile: ", profileId, profileName, e);
failedProfiles++;
}
}
log.info("Tenant profiles update completed. Total: {}, Updated: {}, Skipped: {}, Failed: {}",
totalProfiles, updatedTenantProfiles, skippedProfiles, failedProfiles);
}
private void updateInputNodes() {
log.info("Creating relations for input nodes...");
int n = 0;

207
application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java

@ -0,0 +1,207 @@
/**
* 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.job;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobResult;
import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.Task;
import org.thingsboard.server.common.data.job.task.TaskResult;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.gen.transport.TransportProtos.TaskProto;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.settings.TasksQueueConfig;
import org.thingsboard.server.queue.task.JobStatsService;
import org.thingsboard.server.queue.task.TaskProducerQueueFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
@Slf4j
public class DefaultJobManager implements JobManager {
private final JobService jobService;
private final JobStatsService jobStatsService;
private final PartitionService partitionService;
private final TasksQueueConfig queueConfig;
private final Map<JobType, JobProcessor> jobProcessors;
private final Map<JobType, TbQueueProducer<TbProtoQueueMsg<TaskProto>>> taskProducers;
private final ExecutorService executor;
public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService,
TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig,
List<JobProcessor> jobProcessors) {
this.jobService = jobService;
this.jobStatsService = jobStatsService;
this.partitionService = partitionService;
this.queueConfig = queueConfig;
this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity()));
this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer));
this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass());
}
@Override
public ListenableFuture<Job> submitJob(Job job) {
log.debug("Submitting job: {}", job);
return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor);
}
@Override
public void onJobUpdate(Job job) {
JobStatus status = job.getStatus();
switch (status) {
case PENDING -> {
executor.execute(() -> {
try {
processJob(job);
} catch (Throwable e) {
log.error("Failed to process job update: {}", job, e);
}
});
}
case COMPLETED, FAILED -> {
executor.execute(() -> {
try {
getJobProcessor(job.getType()).onJobFinished(job);
} catch (Throwable e) {
log.error("Failed to process job update: {}", job, e);
}
});
}
}
}
private void processJob(Job job) {
TenantId tenantId = job.getTenantId();
JobId jobId = job.getId();
try {
JobProcessor processor = getJobProcessor(job.getType());
List<TaskResult> toReprocess = job.getConfiguration().getToReprocess();
if (toReprocess == null) {
int tasksCount = processor.process(job, this::submitTask);
log.info("[{}][{}][{}] Submitted {} tasks", tenantId, jobId, job.getType(), tasksCount);
jobStatsService.reportAllTasksSubmitted(tenantId, jobId, tasksCount);
} else {
processor.reprocess(job, toReprocess, this::submitTask);
log.info("[{}][{}][{}] Submitted {} tasks for reprocessing", tenantId, jobId, job.getType(), toReprocess.size());
}
} catch (Throwable e) {
log.error("[{}][{}][{}] Failed to submit tasks", tenantId, jobId, job.getType(), e);
jobService.markAsFailed(tenantId, jobId, e.getMessage());
}
}
@Override
public void cancelJob(TenantId tenantId, JobId jobId) {
log.info("[{}][{}] Cancelling job", tenantId, jobId);
jobService.cancelJob(tenantId, jobId);
}
@Override
public void reprocessJob(TenantId tenantId, JobId jobId) {
log.info("[{}][{}] Reprocessing job", tenantId, jobId);
Job job = jobService.findJobById(tenantId, jobId);
if (job.getStatus() != JobStatus.FAILED) {
throw new IllegalArgumentException("Job is not failed");
}
JobResult result = job.getResult();
if (result.getGeneralError() != null) {
job.presetResult();
} else {
List<TaskResult> taskFailures = result.getResults().stream()
.filter(taskResult -> !taskResult.isSuccess() && !taskResult.isDiscarded())
.toList();
if (result.getFailedCount() > taskFailures.size()) {
throw new IllegalArgumentException("Reprocessing not allowed since there are too many failures (more than " + taskFailures.size() + ")");
}
result.setFailedCount(0);
result.setResults(result.getResults().stream()
.filter(TaskResult::isSuccess)
.toList());
job.getConfiguration().setToReprocess(taskFailures);
}
job.getConfiguration().setTasksKey(UUID.randomUUID().toString());
jobService.saveJob(tenantId, job);
}
private void submitTask(Task<?> task) {
if (ObjectUtils.anyNull(task.getTenantId(), task.getJobId(), task.getKey())) {
throw new IllegalArgumentException("Task " + task + " missing required fields");
}
log.debug("[{}][{}] Submitting task: {}", task.getTenantId(), task.getJobId(), task);
TaskProto taskProto = TaskProto.newBuilder()
.setValue(JacksonUtil.toString(task))
.build();
TbQueueProducer<TbProtoQueueMsg<TaskProto>> producer = taskProducers.get(task.getJobType());
EntityId entityId = null;
if (queueConfig.getPartitioningStrategy().equals("entity")) {
entityId = task.getEntityId();
}
if (entityId == null) {
entityId = task.getTenantId();
}
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TASK_PROCESSOR, task.getJobType().name(), task.getTenantId(), entityId);
producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), taskProto), new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.trace("Submitted task to {}: {}", tpi, taskProto);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to submit task: {}", task, t);
}
});
}
private JobProcessor getJobProcessor(JobType jobType) {
return jobProcessors.get(jobType);
}
@PreDestroy
private void destroy() {
executor.shutdownNow();
}
}

94
application/src/main/java/org/thingsboard/server/service/job/DummyJobProcessor.java

@ -0,0 +1,94 @@
/**
* 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.job;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.job.DummyJobConfiguration;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.DummyTask;
import org.thingsboard.server.common.data.job.task.DummyTaskResult;
import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure;
import org.thingsboard.server.common.data.job.task.Task;
import org.thingsboard.server.common.data.job.task.TaskResult;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@Component
@RequiredArgsConstructor
public class DummyJobProcessor implements JobProcessor {
@Override
public int process(Job job, Consumer<Task<?>> taskConsumer) throws Exception {
DummyJobConfiguration configuration = job.getConfiguration();
if (configuration.getGeneralError() != null) {
for (int number = 1; number <= configuration.getSubmittedTasksBeforeGeneralError(); number++) {
taskConsumer.accept(createTask(job, configuration, number, null, false));
}
Thread.sleep(configuration.getTaskProcessingTimeMs() * (configuration.getSubmittedTasksBeforeGeneralError() / 2)); // sleeping so that some tasks are processed
throw new RuntimeException(configuration.getGeneralError());
}
int taskNumber = 1;
for (int i = 0; i < configuration.getSuccessfulTasksCount(); i++) {
taskConsumer.accept(createTask(job, configuration, taskNumber, null, false));
taskNumber++;
}
if (configuration.getErrors() != null) {
for (int i = 0; i < configuration.getFailedTasksCount(); i++) {
taskConsumer.accept(createTask(job, configuration, taskNumber, configuration.getErrors(), false));
taskNumber++;
}
for (int i = 0; i < configuration.getPermanentlyFailedTasksCount(); i++) {
taskConsumer.accept(createTask(job, configuration, taskNumber, configuration.getErrors(), true));
taskNumber++;
}
}
return configuration.getSuccessfulTasksCount() + configuration.getFailedTasksCount() + configuration.getPermanentlyFailedTasksCount();
}
@Override
public void reprocess(Job job, List<TaskResult> taskFailures, Consumer<Task<?>> taskConsumer) throws Exception {
for (TaskResult taskFailure : taskFailures) {
DummyTaskFailure failure = ((DummyTaskResult) taskFailure).getFailure();
taskConsumer.accept(createTask(job, job.getConfiguration(), failure.getNumber(), failure.isFailAlways() ?
List.of(failure.getError()) : Collections.emptyList(), failure.isFailAlways()));
}
}
private DummyTask createTask(Job job, DummyJobConfiguration configuration, int number, List<String> errors, boolean failAlways) {
return DummyTask.builder()
.tenantId(job.getTenantId())
.jobId(job.getId())
.key(configuration.getTasksKey())
.retries(configuration.getRetries())
.number(number)
.processingTimeMs(configuration.getTaskProcessingTimeMs())
.errors(errors)
.failAlways(failAlways)
.processingTimeoutMs(configuration.getTaskProcessingTimeoutMs())
.build();
}
@Override
public JobType getType() {
return JobType.DUMMY;
}
}

36
application/src/main/java/org/thingsboard/server/service/job/JobProcessor.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.job;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.Task;
import org.thingsboard.server.common.data.job.task.TaskResult;
import java.util.List;
import java.util.function.Consumer;
public interface JobProcessor {
int process(Job job, Consumer<Task<?>> taskConsumer) throws Exception;
void reprocess(Job job, List<TaskResult> taskFailures, Consumer<Task<?>> taskConsumer) throws Exception;
default void onJobFinished(Job job) {}
JobType getType();
}

115
application/src/main/java/org/thingsboard/server/service/job/JobStatsProcessor.java

@ -0,0 +1,115 @@
/**
* 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.job;
import jakarta.annotation.PreDestroy;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.JobStats;
import org.thingsboard.server.common.data.job.task.TaskResult;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.gen.transport.TransportProtos.JobStatsMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.settings.TasksQueueConfig;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@TbCoreComponent
@Component
@Slf4j
public class JobStatsProcessor {
private final JobService jobService;
private final TasksQueueConfig queueConfig;
private final QueueConsumerManager<TbProtoQueueMsg<JobStatsMsg>> jobStatsConsumer;
private final ExecutorService consumerExecutor;
public JobStatsProcessor(JobService jobService,
TasksQueueConfig queueConfig,
TbCoreQueueFactory queueFactory) {
this.jobService = jobService;
this.queueConfig = queueConfig;
this.consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("job-stats-consumer"));
this.jobStatsConsumer = QueueConsumerManager.<TbProtoQueueMsg<JobStatsMsg>>builder()
.name("job-stats")
.msgPackProcessor(this::processStats)
.pollInterval(queueConfig.getStatsPollInterval())
.consumerCreator(queueFactory::createJobStatsConsumer)
.consumerExecutor(consumerExecutor)
.build();
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void afterStartUp() {
jobStatsConsumer.subscribe();
jobStatsConsumer.launch();
}
@SneakyThrows
private void processStats(List<TbProtoQueueMsg<JobStatsMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<JobStatsMsg>> consumer) {
Map<JobId, JobStats> stats = new HashMap<>();
for (TbProtoQueueMsg<JobStatsMsg> msg : msgs) {
JobStatsMsg statsMsg = msg.getValue();
TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB()));
JobId jobId = new JobId(new UUID(statsMsg.getJobIdMSB(), statsMsg.getJobIdLSB()));
JobStats jobStats = stats.computeIfAbsent(jobId, __ -> new JobStats(tenantId, jobId));
if (statsMsg.hasTaskResult()) {
TaskResult taskResult = JacksonUtil.fromString(statsMsg.getTaskResult().getValue(), TaskResult.class);
jobStats.getTaskResults().add(taskResult);
}
if (statsMsg.hasTotalTasksCount()) {
jobStats.setTotalTasksCount(statsMsg.getTotalTasksCount());
}
}
stats.forEach((jobId, jobStats) -> {
TenantId tenantId = jobStats.getTenantId();
try {
log.debug("[{}][{}] Processing job stats: {}", tenantId, jobId, stats);
jobService.processStats(tenantId, jobId, jobStats);
} catch (Exception e) {
log.error("[{}][{}] Failed to process job stats: {}", tenantId, jobId, jobStats, e);
}
});
consumer.commit();
Thread.sleep(queueConfig.getStatsProcessingInterval());
}
@PreDestroy
private void destroy() {
jobStatsConsumer.stop();
consumerExecutor.shutdownNow();
}
}

59
application/src/main/java/org/thingsboard/server/service/job/task/DummyTaskProcessor.java

@ -0,0 +1,59 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.job.task;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.DummyTask;
import org.thingsboard.server.common.data.job.task.DummyTaskResult;
import org.thingsboard.server.common.data.job.task.Task;
import org.thingsboard.server.queue.task.TaskProcessor;
import java.util.Map;
import java.util.concurrent.Future;
public class DummyTaskProcessor extends TaskProcessor<DummyTask, DummyTaskResult> {
@Override
public DummyTaskResult process(DummyTask task) throws Exception {
if (task.getProcessingTimeMs() > 0) {
Thread.sleep(task.getProcessingTimeMs());
}
if (task.isFailAlways()) {
throw new RuntimeException(task.getErrors().get(0));
}
if (task.getErrors() != null && task.getAttempt() <= task.getErrors().size()) {
String error = task.getErrors().get(task.getAttempt() - 1);
throw new RuntimeException(error);
}
return DummyTaskResult.success(task);
}
@Override
public long getProcessingTimeout(DummyTask task) {
return task.getProcessingTimeoutMs() > 0 ? task.getProcessingTimeoutMs() : 2000;
}
public Map<Object, Pair<Task<DummyTaskResult>, Future<DummyTaskResult>>> getCurrentTasks() {
return currentTasks;
}
@Override
public JobType getJobType() {
return JobType.DUMMY;
}
}

50
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java

@ -0,0 +1,50 @@
/**
* 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.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.ResourcesShortageNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig;
@Service
@RequiredArgsConstructor
public class ResourcesShortageTriggerProcessor implements NotificationRuleTriggerProcessor<ResourcesShortageTrigger, ResourcesShortageNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(ResourcesShortageTrigger trigger, ResourcesShortageNotificationRuleTriggerConfig triggerConfig) {
float usagePercent = trigger.getUsage() / 100.0f;
return switch (trigger.getResource()) {
case CPU -> usagePercent >= triggerConfig.getCpuThreshold();
case RAM -> usagePercent >= triggerConfig.getRamThreshold();
case STORAGE -> usagePercent >= triggerConfig.getStorageThreshold();
};
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) {
return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RESOURCES_SHORTAGE;
}
}

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

@ -141,7 +141,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
}
}
private void processMsgs(List<TbProtoQueueMsg<ToCalculatedFieldMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumer, QueueConfig config) throws Exception {
private void processMsgs(List<TbProtoQueueMsg<ToCalculatedFieldMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumer, Object consumerKey, QueueConfig config) throws Exception {
List<IdMsgPair<ToCalculatedFieldMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToCalculatedFieldMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));

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

@ -577,7 +577,8 @@ public class DefaultTbClusterService implements TbClusterService {
}
}
private void broadcast(ComponentLifecycleMsg msg) {
@Override
public void broadcast(ComponentLifecycleMsg msg) {
ComponentLifecycleMsgProto componentLifecycleMsgProto = toProto(msg);
TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE);
@ -592,6 +593,7 @@ public class DefaultTbClusterService implements TbClusterService {
|| entityType.equals(EntityType.ENTITY_VIEW)
|| entityType.equals(EntityType.NOTIFICATION_RULE)
|| entityType.equals(EntityType.CALCULATED_FIELD)
|| entityType.equals(EntityType.JOB)
) {
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);

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

@ -255,7 +255,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
.collect(Collectors.toSet()));
}
private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, QueueConfig config) throws Exception {
private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, Object consumerKey, QueueConfig config) throws Exception {
List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));

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

@ -126,7 +126,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
mainConsumer.update(partitions);
}
private void processMsgs(List<TbProtoQueueMsg<ToEdgeMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeMsg>> consumer, QueueConfig edgeQueueConfig) throws InterruptedException {
private void processMsgs(List<TbProtoQueueMsg<ToEdgeMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeMsg>> consumer, Object consumerKey, QueueConfig edgeQueueConfig) throws InterruptedException {
List<IdMsgPair<ToEdgeMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToEdgeMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));

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

@ -16,7 +16,9 @@
package org.thingsboard.server.service.queue;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
@ -30,11 +32,9 @@ import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
@ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core' || '${service.type:null}'=='tb-rule-engine'")
public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService {
private final TbTenantProfileCache tenantProfileCache;
public DefaultTenantRoutingInfoService(TbTenantProfileCache tenantProfileCache) {
this.tenantProfileCache = tenantProfileCache;
}
@Lazy
@Autowired
private TbTenantProfileCache tenantProfileCache;
@Override
public TenantRoutingInfo getRoutingInfo(TenantId tenantId) {

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

@ -127,6 +127,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
@Override
protected void processMsgs(List<TbProtoQueueMsg<ToRuleEngineMsg>> msgs,
TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer,
Object consumerKey,
Queue queue) throws Exception {
TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue);
TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue);

34
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java

@ -20,7 +20,6 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@ -34,8 +33,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@ -48,17 +47,16 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.entitiy.tenant.TbTenantService;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public abstract class AbstractOAuth2ClientMapper {
private static final int DASHBOARDS_REQUEST_LIMIT = 10;
@Autowired
@ -79,18 +77,12 @@ public abstract class AbstractOAuth2ClientMapper {
@Autowired
private DashboardService dashboardService;
@Autowired
private InstallScripts installScripts;
@Autowired
private TbUserService tbUserService;
@Autowired
protected TbTenantProfileCache tenantProfileCache;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Value("${edges.enabled}")
@Getter
private boolean edgesEnabled;
@ -120,8 +112,7 @@ public abstract class AbstractOAuth2ClientMapper {
} else {
user.setAuthority(Authority.CUSTOMER_USER);
}
TenantId tenantId = oauth2User.getTenantId() != null ?
oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName());
TenantId tenantId = oauth2User.getTenantId() != null ? oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName());
user.setTenantId(tenantId);
CustomerId customerId = oauth2User.getCustomerId() != null ?
oauth2User.getCustomerId() : getCustomerId(user.getTenantId(), oauth2User.getCustomerName());
@ -173,17 +164,15 @@ public abstract class AbstractOAuth2ClientMapper {
}
}
private TenantId getTenantId(String tenantName) throws Exception {
List<Tenant> tenants = tenantService.findTenants(new PageLink(1, 0, tenantName)).getData();
Tenant tenant;
if (tenants == null || tenants.isEmpty()) {
tenant = new Tenant();
tenant.setTitle(tenantName);
tenant = tbTenantService.save(tenant);
} else {
tenant = tenants.get(0);
private TenantId getTenantId(String name) throws Exception {
Tenant tenant = tenantService.findTenantByName(name);
if (tenant != null) {
return tenant.getId();
}
return tenant.getTenantId();
tenant = new Tenant();
tenant.setTitle(name);
tenant = tbTenantService.save(tenant);
return tenant.getId();
}
private CustomerId getCustomerId(TenantId tenantId, String customerName) {
@ -220,4 +209,5 @@ public abstract class AbstractOAuth2ClientMapper {
} while (dashboardsPage.hasNext());
return Optional.empty();
}
}

3
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -50,7 +50,8 @@ public enum Resource {
VERSION_CONTROL,
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS;
MOBILE_APP_SETTINGS,
JOB(EntityType.JOB);
private final Set<EntityType> entityTypes;

1
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -55,6 +55,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ));
put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.JOB, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

4
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -348,7 +348,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (sub.isLatestValues()) {
for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey());
if (stateTs == null || kv.getTs() > stateTs) {
if (stateTs == null || kv.getTs() >= stateTs) {
if (updateData == null) {
updateData = new ArrayList<>();
}
@ -362,7 +362,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey());
if (stateTs != null) {
if (!sub.isLatestValues() || kv.getTs() > stateTs) {
if (!sub.isLatestValues() || kv.getTs() >= stateTs) {
if (updateData == null) {
updateData = new ArrayList<>();
}

12
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java

@ -44,6 +44,7 @@ import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -154,9 +155,8 @@ public abstract class TbAbstractEntityQuerySubCtx<T extends EntityCountQuery> ex
private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,
Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {
Map<String, TsValue> latestUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {
Object[] data = (Object[]) v.get(0);
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
subscriptionUpdate.getValues().forEach((key, values) -> {
latestUpdate.put(key, getLatest(values));
});
boolean invalidateFilter = false;
@ -283,6 +283,12 @@ public abstract class TbAbstractEntityQuerySubCtx<T extends EntityCountQuery> ex
}
}
protected TsValue getLatest(List<TsValue> values) {
return values.stream()
.max(Comparator.comparing(TsValue::getTs))
.orElse(null);
}
@Data
public static class DynamicValueKey {
@Getter

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

@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
@ -191,9 +190,8 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
EntityId entityId = subToEntityIdMap.get(subscriptionUpdate.getSubscriptionId());
if (entityId != null) {
Map<String, TsValue> latestUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {
Object[] data = (Object[]) v.get(0);
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
subscriptionUpdate.getValues().forEach((key, values) -> {
latestUpdate.put(key, getLatest(values));
});
EntityData entityData = entitiesMap.get(entityId);
entityData.getLatest().computeIfAbsent(keyType, tmp -> new HashMap<>()).putAll(latestUpdate);

71
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java

@ -43,7 +43,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -94,35 +93,32 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
private void sendLatestWsMsg(EntityId entityId, String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) {
Map<String, TsValue> latestUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {
Object[] data = (Object[]) v.get(0);
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
subscriptionUpdate.getValues().forEach((key, values) -> {
latestUpdate.put(key, getLatest(values));
});
EntityData entityData = getDataForEntity(entityId);
if (entityData != null && entityData.getLatest() != null) {
Map<String, TsValue> latestCtxValues = entityData.getLatest().get(keyType);
Map<String, TsValue> latestCtxValues = entityData.getLatest().computeIfAbsent(keyType, __ -> new HashMap<>());
log.trace("[{}][{}][{}] Going to compare update with {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), latestCtxValues);
if (latestCtxValues != null) {
latestCtxValues.forEach((k, v) -> {
TsValue update = latestUpdate.get(k);
if (update != null) {
//Ignore notifications about deleted keys
if (!(update.getTs() == 0 && (update.getValue() == null || update.getValue().isEmpty()))) {
if (update.getTs() < v.getTs()) {
log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
latestUpdate.remove(k);
} else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) {
log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
latestUpdate.remove(k);
}
} else {
log.trace("[{}][{}][{}] Received deleted notification for: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k);
latestCtxValues.forEach((k, v) -> {
TsValue update = latestUpdate.get(k);
if (update != null) {
//Ignore notifications about deleted keys
if (!(update.getTs() == 0 && (update.getValue() == null || update.getValue().isEmpty()))) {
if (update.getTs() < v.getTs()) {
log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
latestUpdate.remove(k);
} else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) {
log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
latestUpdate.remove(k);
}
} else {
log.trace("[{}][{}][{}] Received deleted notification for: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k);
}
});
//Setting new values
latestUpdate.forEach(latestCtxValues::put);
}
}
});
//Setting new values
latestCtxValues.putAll(latestUpdate);
}
if (!latestUpdate.isEmpty()) {
Map<EntityKeyType, Map<String, TsValue>> latestMap = Collections.singletonMap(keyType, latestUpdate);
@ -133,40 +129,39 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
private void sendTsWsMsg(EntityId entityId, String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType) {
Map<String, List<TsValue>> tsUpdate = new HashMap<>();
subscriptionUpdate.getData().forEach((k, v) -> {
Object[] data = (Object[]) v.get(0);
tsUpdate.computeIfAbsent(k, key -> new ArrayList<>()).add(new TsValue((Long) data[0], (String) data[1]));
subscriptionUpdate.getValues().forEach((key, values) -> {
tsUpdate.put(key, new ArrayList<>(values));
});
Map<String, TsValue> latestCtxValues = getLatestTsValuesForEntity(entityId);
log.trace("[{}][{}][{}] Going to compare update with {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), latestCtxValues);
if (latestCtxValues != null) {
latestCtxValues.forEach((k, v) -> {
List<TsValue> updateList = tsUpdate.get(k);
latestCtxValues.forEach((key, latest) -> {
List<TsValue> updateList = tsUpdate.get(key);
if (updateList != null) {
for (TsValue update : new ArrayList<>(updateList)) {
if (update.getTs() < v.getTs()) {
log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
if (update.getTs() < latest.getTs()) {
log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), key, update.getTs());
// Looks like this is redundant feature and our UI is ready to merge the updates.
//updateList.remove(update);
} else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) {
log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
} else if ((update.getTs() == latest.getTs() && update.getValue().equals(latest.getValue()))) {
log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), key, update.getTs());
updateList.remove(update);
}
if (updateList.isEmpty()) {
tsUpdate.remove(k);
tsUpdate.remove(key);
}
}
}
});
//Setting new values
tsUpdate.forEach((k, v) -> {
Optional<TsValue> maxValue = v.stream().max(Comparator.comparingLong(TsValue::getTs));
maxValue.ifPresent(max -> latestCtxValues.put(k, max));
tsUpdate.forEach((key, values) -> {
values.stream().max(Comparator.comparingLong(TsValue::getTs))
.ifPresent(latest -> latestCtxValues.put(key, latest));
});
}
if (!tsUpdate.isEmpty()) {
Map<String, TsValue[]> tsMap = new HashMap<>();
tsUpdate.forEach((key, tsValue) -> tsMap.put(key, tsValue.toArray(new TsValue[tsValue.size()])));
tsUpdate.forEach((key, values) -> tsMap.put(key, values.toArray(new TsValue[0])));
EntityData entityData = new EntityData(entityId, null, tsMap);
sendWsMsg(new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription));
}

30
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -39,6 +39,9 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.domain.DomainService;
@ -92,6 +95,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private final DomainService domainService;
private final MailService mailService;
private final SmsService smsService;
private final NotificationRuleProcessor notificationRuleProcessor;
private volatile ScheduledExecutorService scheduler;
@Value("${metrics.system_info.persist_frequency:60}")
@ -163,7 +167,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
if (twoFaSettings != null) {
var providers = twoFaSettings.getJsonValue().get("providers");
if (providers != null) {
return providers.size() > 0;
return !providers.isEmpty();
}
}
return false;
@ -180,6 +184,11 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private void saveCurrentClusterSystemInfo() {
long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
clusterSystemData.forEach(data -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(data.getCpuUsage()).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(data.getMemoryUsage()).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(data.getDiscUsage()).build());
});
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
}
@ -188,9 +197,21 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
long ts = System.currentTimeMillis();
List<TsKvEntry> tsList = new ArrayList<>();
tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false)));
getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v))));
getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v))));
getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (long) v))));
getCpuUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).build());
});
getMemoryUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).build());
});
getDiscSpaceUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).build());
});
getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v))));
getTotalMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalMemory", v))));
@ -244,4 +265,5 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
scheduler = null;
}
}
}

92
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTbTelemetryService.java

@ -0,0 +1,92 @@
/**
* 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.telemetry;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AggregationParams;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.ValidationResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class DefaultTbTelemetryService implements TbTelemetryService {
private final TimeseriesService tsService;
private final AccessValidator accessValidator;
@Override
public ListenableFuture<List<TsKvEntry>> getTimeseries(EntityId entityId, List<String> keys, Long startTs, Long endTs, IntervalType intervalType,
Long interval, String timeZone, Integer limit, Aggregation agg, String orderBy,
Boolean useStrictDataTypes, SecurityUser currentUser) {
SettableFuture<List<TsKvEntry>> future = SettableFuture.create();
accessValidator.validate(currentUser, Operation.READ_TELEMETRY, entityId, new FutureCallback<>() {
@Override
public void onSuccess(ValidationResult validationResult) {
try {
AggregationParams params;
if (Aggregation.NONE.equals(agg)) {
params = AggregationParams.none();
} else if (intervalType == null || IntervalType.MILLISECONDS.equals(intervalType)) {
params = interval == 0L ? AggregationParams.none() : AggregationParams.milliseconds(agg, interval);
} else {
params = AggregationParams.calendar(agg, intervalType, timeZone);
}
List<ReadTsKvQuery> queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, params, limit, orderBy)).collect(Collectors.toList());
Futures.addCallback(tsService.findAll(currentUser.getTenantId(), entityId, queries), new FutureCallback<>() {
@Override
public void onSuccess(List<TsKvEntry> result) {
future.set(result);
}
@Override
public void onFailure(Throwable t) {
future.setException(t);
}
}, MoreExecutors.directExecutor());
} catch (Throwable e) {
onFailure(e);
}
}
@Override
public void onFailure(Throwable t) {
future.setException(t);
}
});
return future;
}
}

43
application/src/main/java/org/thingsboard/server/service/telemetry/TbTelemetryService.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.List;
public interface TbTelemetryService {
ListenableFuture<List<TsKvEntry>> getTimeseries(EntityId entityId,
List<String> keys,
Long startTs,
Long endTs,
IntervalType intervalType,
Long interval,
String timeZone,
Integer limit,
Aggregation agg,
String orderBy,
Boolean useStrictDataTypes,
SecurityUser currentUser) throws ThingsboardException;
}

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

@ -274,7 +274,7 @@ public class DefaultWebSocketService implements WebSocketService {
@Override
public void sendUpdate(String sessionId, int cmdId, TelemetrySubscriptionUpdate update) {
// We substitute the subscriptionId with cmdId for old-style subscriptions.
doSendUpdate(sessionId, cmdId, update.copyWithNewSubscriptionId(cmdId));
doSendUpdate(sessionId, cmdId, update.withSubscriptionId(cmdId));
}
@Override

50
application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java

@ -15,20 +15,28 @@
*/
package org.thingsboard.server.service.ws.telemetry.sub;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.With;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
@Getter
@AllArgsConstructor
public class TelemetrySubscriptionUpdate {
@With
private final int subscriptionId;
private int errorCode;
private String errorMsg;
@ -66,11 +74,28 @@ public class TelemetrySubscriptionUpdate {
this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg();
}
public int getSubscriptionId() {
return subscriptionId;
}
@JsonIgnore
public Map<String, List<TsValue>> getValues() {
if (data == null || data.isEmpty()) {
return Collections.emptyMap();
}
public Map<String, List<Object>> getData() {
Map<String, List<TsValue>> data = new HashMap<>();
this.data.forEach((key, entries) -> {
if (entries.isEmpty()) {
return;
}
List<TsValue> values = new ArrayList<>(entries.size());
entries.forEach(object -> {
if (!(object instanceof Object[] entry) || entry.length < 2) {
return;
}
TsValue tsValue = new TsValue((Long) entry[0], (String) entry[1]);
values.add(tsValue);
});
data.put(key, values);
});
return data;
}
@ -86,28 +111,17 @@ public class TelemetrySubscriptionUpdate {
}
}
public int getErrorCode() {
return errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public TelemetrySubscriptionUpdate copyWithNewSubscriptionId(int subscriptionId){
return new TelemetrySubscriptionUpdate(subscriptionId, errorCode, errorMsg, data);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder("TelemetrySubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=");
data.forEach((k, v) -> {
result.append(k).append("=[");
for(Object a : v){
result.append(Arrays.toString((Object[])a)).append("|");
for (Object a : v) {
result.append(Arrays.toString((Object[]) a)).append("|");
}
result.append("]");
});
return result.toString();
}
}

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

@ -1631,6 +1631,11 @@ queue:
- key: max.poll.records
# Max poll records for edqs.state topic
value: "${TB_QUEUE_KAFKA_EDQS_STATE_MAX_POLL_RECORDS:512}"
tasks:
# Key-value properties for Kafka consumer for tasks topics
- key: max.poll.records
# Max poll records for tasks topics
value: "${TB_QUEUE_KAFKA_TASKS_MAX_POLL_RECORDS:1}"
# If you override any default Kafka topic name using environment variables, you must also specify the related consumer properties
# for the new topic in `consumer-properties-per-topic-inline`. Otherwise, the topic will not inherit its expected configuration (e.g., max.poll.records, timeouts, etc).
# Each entry sets a single property for a specific topic. To define multiple properties for a topic, repeat the topic key.
@ -1676,6 +1681,8 @@ queue:
edqs-requests: "${TB_QUEUE_KAFKA_EDQS_REQUESTS_TOPIC_PROPERTIES:retention.ms:180000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for EDQS state topic (infinite retention, compaction)
edqs-state: "${TB_QUEUE_KAFKA_EDQS_STATE_TOPIC_PROPERTIES:retention.ms:-1;segment.bytes:52428800;retention.bytes:-1;partitions:1;min.insync.replicas:1;cleanup.policy:compact}"
# Kafka properties for tasks topics
tasks: "${TB_QUEUE_KAFKA_TASKS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:1;min.insync.replicas:1}"
consumer-stats:
# Prints lag between consumer group offset and last messages offset in Kafka topics
enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}"
@ -1764,6 +1771,8 @@ queue:
supported: "${TB_EDQS_API_SUPPORTED:false}"
# Whether to auto-enable EDQS API (if queue.edqs.api.supported is true) when sync of data to Kafka is finished
auto_enable: "${TB_EDQS_API_AUTO_ENABLE:true}"
# Interval in milliseconds to check for ready EDQS servers
readiness_check_interval: "${TB_EDQS_READINESS_CHECK_INTERVAL_MS:60000}"
# Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices)
mode: "${TB_EDQS_MODE:local}"
local:
@ -1787,6 +1796,10 @@ queue:
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
# Maximum timeout for requests to EDQS
max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}"
# Thread pool size for EDQS requests executor
request_executor_size: "${TB_EDQS_REQUEST_EXECUTOR_SIZE:50}"
# Time to live for EDQS versions cache in minutes. Must be bigger than the time taken for the sync process.
versions_cache_ttl: "${TB_EDQS_VERSIONS_CACHE_TTL_MINUTES:60}"
# Strings longer than this threshold will be compressed
string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}"
stats:
@ -1890,6 +1903,23 @@ queue:
enabled: "${TB_QUEUE_EDGE_STATS_ENABLED:true}"
# Statistics printing interval for Edge services
print-interval-ms: "${TB_QUEUE_EDGE_STATS_PRINT_INTERVAL_MS:60000}"
tasks:
# Poll interval in milliseconds for tasks topics
poll_interval: "${TB_QUEUE_TASKS_POLL_INTERVAL_MS:500}"
# Partitions count for tasks queues
partitions: "${TB_QUEUE_TASKS_PARTITIONS:12}"
# Custom partitions count for tasks queues per type. Format: 'TYPE1:24;TYPE2:36', e.g. 'CF_REPROCESSING:24;TENANT_EXPORT:6'
partitions_per_type: "${TB_QUEUE_TASKS_PARTITIONS_PER_TYPE:}"
# Tasks partitioning strategy: 'tenant' or 'entity'. By default, using 'tenant' - tasks of a specific tenant are processed in the same partition.
# In a single-tenant environment, use 'entity' strategy to distribute the tasks among multiple partitions.
partitioning_strategy: "${TB_QUEUE_TASKS_PARTITIONING_STRATEGY:tenant}"
stats:
# Name for the tasks stats topic
topic: "${TB_QUEUE_TASKS_STATS_TOPIC:jobs.stats}"
# Poll interval in milliseconds for tasks stats topic
poll_interval: "${TB_QUEUE_TASKS_STATS_POLL_INTERVAL_MS:500}"
# Interval in milliseconds to process job stats
processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}"
# Event configuration parameters
event:

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

@ -48,6 +48,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.web.MockMultipartFile;
@ -102,10 +103,13 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType;
@ -128,6 +132,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority;
@ -164,6 +169,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@ -278,6 +284,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired
protected InMemoryStorage storage;
@Autowired
protected JdbcTemplate jdbcTemplate;
@MockBean
protected CfRocksDb cfRocksDb;
@ -389,6 +398,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID);
jdbcTemplate.execute("TRUNCATE TABLE notification");
log.info("Executed web test teardown");
}
@ -1253,4 +1264,26 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
new PageLink(limit, 0), unreadOnly, deliveryMethod).getData();
}
protected Job findJobById(JobId jobId) throws Exception {
return doGet("/api/job/" + jobId, Job.class);
}
protected List<Job> findJobs() throws Exception {
return doGetTypedWithPageLink("/api/jobs?", new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData();
}
protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception {
return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) +
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData();
}
protected void cancelJob(JobId jobId) throws Exception {
doPost("/api/job/" + jobId + "/cancel").andExpect(status().isOk());
}
protected void reprocessJob(JobId jobId) throws Exception {
doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk());
}
}

81
application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java

@ -15,21 +15,27 @@
*/
package org.thingsboard.server.controller;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.state.EdqsStateService;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
@DaoSqlTest
@ -39,22 +45,23 @@ import static org.awaitility.Awaitility.await;
"queue.edqs.sync.enabled=true",
"queue.edqs.api.supported=true",
"queue.edqs.api.auto_enable=true",
"queue.edqs.mode=local"
"queue.edqs.mode=local",
"queue.edqs.readiness_check_interval=500"
})
public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
@Autowired
private EdqsApiService edqsApiService;
private EdqsService edqsService;
@Autowired
private EdqsStateService edqsStateService;
private DiscoveryService discoveryService;
@MockBean // so that we don't do backup for tests
private EdqsRocksDb edqsRocksDb;
@Before
public void before() {
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsApiService.isEnabled() && edqsStateService.isReady());
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled());
}
@Override
@ -69,4 +76,66 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
result -> result == expectedResult);
}
@Test
public void testEdqsState() throws Exception {
loginSysAdmin();
assertThat(getEdqsState().getApiMode()).isEqualTo(EdqsApiMode.AUTO_ENABLED);
// notifying EDQS is not ready: API should be auto-disabled
discoveryService.setReady(false);
verifyState(state -> {
assertThat(state.getApiMode()).isEqualTo(EdqsApiMode.AUTO_DISABLED);
assertThat(state.getEdqsReady()).isFalse();
assertThat(state.isApiEnabled()).isFalse();
});
// manually disabling API
edqsService.processSystemRequest(ToCoreEdqsRequest.builder()
.apiEnabled(false)
.build());
verifyState(state -> {
assertThat(state.getApiMode()).isEqualTo(EdqsApiMode.DISABLED);
assertThat(state.getEdqsReady()).isFalse();
assertThat(state.isApiEnabled()).isFalse();
});
// notifying EDQS is ready: API should not be enabled automatically because manually disabled previously
discoveryService.setReady(true);
verifyState(state -> {
assertThat(state.getEdqsReady()).isTrue();
assertThat(state.getApiMode()).isEqualTo(EdqsApiMode.DISABLED);
assertThat(state.isApiEnabled()).isFalse();
});
// manually enabling API
edqsService.processSystemRequest(ToCoreEdqsRequest.builder()
.apiEnabled(true)
.build());
verifyState(state -> {
assertThat(state.getApiMode()).isEqualTo(EdqsApiMode.ENABLED);
assertThat(state.getEdqsReady()).isTrue();
assertThat(state.isApiEnabled()).isTrue();
});
// notifying EDQS is not ready: API should be auto-disabled
discoveryService.setReady(false);
verifyState(state -> {
assertThat(state.getApiMode()).isEqualTo(EdqsApiMode.AUTO_DISABLED);
assertThat(state.getEdqsReady()).isFalse();
assertThat(state.isApiEnabled()).isFalse();
});
discoveryService.setReady(true);
}
private void verifyState(ThrowingConsumer<EdqsState> assertion) {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(getEdqsState()).satisfies(assertion);
});
}
private EdqsState getEdqsState() throws Exception {
return doGet("/api/edqs/state", EdqsState.class);
}
}

7
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -22,6 +22,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.ResultActions;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
@ -78,6 +79,10 @@ import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"queue.edqs.sync.enabled=true", // only enabling sync
"queue.edqs.api.supported=false",
})
public class EntityQueryControllerTest extends AbstractControllerTest {
private static final String CUSTOMER_USER_EMAIL = "entityQueryCustomer@thingsboard.org";
@ -803,7 +808,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
//assign dashboard
doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+ "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+ "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
// check entity data query by customer
User customerUser = new User();

38
application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java

@ -36,9 +36,11 @@ import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.validation.RateLimit;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.TbQueueCallback;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -314,6 +316,42 @@ public class TenantProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, pageData.getTotalElements());
}
@Test
public void testRateLimitValidationAllFields() throws Exception {
loginSysAdmin();
Mockito.reset(tbClusterService);
List<String> failedFields = new ArrayList<>();
for (Field field : DefaultTenantProfileConfiguration.class.getDeclaredFields()) {
RateLimit rateLimit = field.getAnnotation(RateLimit.class);
if (rateLimit == null) continue;
String fieldName = field.getName();
String expectedLabel = rateLimit.fieldName();
TenantProfile tenantProfile = createTenantProfile("Invalid RateLimit - " + fieldName);
DefaultTenantProfileConfiguration config = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration();
field.setAccessible(true);
field.set(config, "10:1,10:1"); // Set invalid duplicate value
try {
doPost("/api/tenantProfile", tenantProfile)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(expectedLabel + " rate limit has duplicate 'Per seconds' configuration.")));
} catch (AssertionError e) {
failedFields.add(fieldName + " (label: " + expectedLabel + ")");
}
}
if (!failedFields.isEmpty()) {
throw new AssertionError("RateLimit validation failed for fields: " + String.join(", ", failedFields));
}
testBroadcastEntityStateChangeEventNeverTenantProfile();
}
private TenantProfile createTenantProfile(String name) {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName(name);

8
application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java

@ -27,6 +27,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class TrendzControllerTest extends AbstractControllerTest {
private final String trendzUrl = "https://some.domain.com:18888/also_necessary_prefix";
private final String apiKey = "$2a$10$iDjfqYmnrw9gkdw4XhgzFOU.R/pVz3OKgXOdpbR2LuXaKatGcGLiG";
@Before
public void setUp() throws Exception {
@ -35,6 +36,7 @@ public class TrendzControllerTest extends AbstractControllerTest {
TrendzSettings trendzSettings = new TrendzSettings();
trendzSettings.setEnabled(true);
trendzSettings.setBaseUrl(trendzUrl);
trendzSettings.setApiKey(apiKey);
doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk());
}
@ -48,9 +50,12 @@ public class TrendzControllerTest extends AbstractControllerTest {
assertThat(trendzSettings).isNotNull();
assertThat(trendzSettings.isEnabled()).isTrue();
assertThat(trendzSettings.getBaseUrl()).isEqualTo(trendzUrl);
trendzSettings.setApiKey(apiKey);
String updatedUrl = "https://some.domain.com:18888/tenant_trendz";
String updatedApiKey = "$2a$10$aRR0bHa8rtzP5jRcE72vp.hRFsGQz4MGIs62oogLbfOCFK3.RIESG";
trendzSettings.setBaseUrl(updatedUrl);
trendzSettings.setApiKey(updatedApiKey);
doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk());
@ -65,6 +70,7 @@ public class TrendzControllerTest extends AbstractControllerTest {
TrendzSettings newTrendzSettings = new TrendzSettings();
newTrendzSettings.setEnabled(true);
newTrendzSettings.setBaseUrl("https://some.domain.com:18888/customer_trendz");
newTrendzSettings.setApiKey("some_api_key");
doPost("/api/trendz/settings", newTrendzSettings).andExpect(status().isForbidden());
@ -72,6 +78,6 @@ public class TrendzControllerTest extends AbstractControllerTest {
assertThat(fetchedTrendzSettings).isNotNull();
assertThat(fetchedTrendzSettings.isEnabled()).isTrue();
assertThat(fetchedTrendzSettings.getBaseUrl()).isEqualTo(trendzUrl);
assertThat(fetchedTrendzSettings.getApiKey()).isEqualTo(apiKey);
}
}

9
application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java

@ -49,6 +49,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
@ -419,10 +420,11 @@ public class HashPartitionServiceTest {
}
private HashPartitionService createPartitionService() {
HashPartitionService partitionService = new HashPartitionService(serviceInfoProvider,
routingInfoService,
HashPartitionService partitionService = new HashPartitionService(
applicationEventPublisher,
queueRoutingInfoService,
serviceInfoProvider,
Optional.of(routingInfoService),
Optional.of(queueRoutingInfoService),
topicService);
ReflectionTestUtils.setField(partitionService, "coreTopic", "tb.core");
ReflectionTestUtils.setField(partitionService, "corePartitions", 10);
@ -434,6 +436,7 @@ public class HashPartitionServiceTest {
ReflectionTestUtils.setField(partitionService, "edgeTopic", "tb.edge");
ReflectionTestUtils.setField(partitionService, "edgePartitions", 10);
ReflectionTestUtils.setField(partitionService, "edqsPartitions", 12);
ReflectionTestUtils.setField(partitionService, "defaultTasksPartitions", 12);
partitionService.init();
partitionService.partitionsInit();
return partitionService;

9
application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
@ -53,19 +53,20 @@ import static org.awaitility.Awaitility.await;
"queue.edqs.sync.enabled=true",
"queue.edqs.api.supported=true",
"queue.edqs.api.auto_enable=true",
"queue.edqs.mode=local"
"queue.edqs.mode=local",
"queue.edqs.readiness_check_interval=1000"
})
public class EdqsEntityServiceTest extends EntityServiceTest {
@Autowired
private EdqsApiService edqsApiService;
private EdqsService edqsService;
@MockBean
private EdqsRocksDb edqsRocksDb;
@Before
public void beforeEach() {
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsApiService.isEnabled());
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled());
}
// sql implementation has a bug with data duplication, edqs implementation returns correct value

504
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java

@ -0,0 +1,504 @@
/**
* 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.job;
import lombok.SneakyThrows;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.DummyJobConfiguration;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobFilter;
import org.thingsboard.server.common.data.job.JobResult;
import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.DummyTaskResult;
import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.job.JobDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.task.JobStatsService;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"queue.tasks.stats.processing_interval=0"
})
public class JobManagerTest extends AbstractControllerTest {
@Autowired
private JobManager jobManager;
@SpyBean
private TestTaskProcessor taskProcessor;
@SpyBean
private JobStatsService jobStatsService;
@Autowired
private JobDao jobDao;
private TenantId tenantId;
private Device jobEntity;
@Before
public void setUp() throws Exception {
loginTenantAdmin();
tenantId = super.tenantId;
jobEntity = createDevice("Test", "Test");
}
@After
public void tearDown() throws Exception {
}
@Test
public void testSubmitJob_allTasksSuccessful() {
int tasksCount = 5;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(1000)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
});
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED);
assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount);
assertThat(job.getResult().getResults()).isEmpty();
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount);
});
}
@Test
public void testSubmitJob_someTasksPermanentlyFailed() {
int successfulTasks = 3;
int failedTasks = 2;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(successfulTasks)
.failedTasksCount(failedTasks)
.errors(List.of("error1", "error2", "error3"))
.retries(2)
.taskProcessingTimeMs(100)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
JobResult jobResult = job.getResult();
assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks);
assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks);
assertThat(jobResult.getTotalCount()).isEqualTo(successfulTasks + failedTasks);
assertThat(getFailures(jobResult)).hasSize(2).allSatisfy(failure -> {
assertThat(failure.getError()).isEqualTo("error3"); // last error
});
assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount());
});
}
@Test
public void testSubmitJob_taskTimeout() {
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(1)
.taskProcessingTimeMs(5000) // bigger than DummyTaskProcessor.getTaskProcessingTimeout()
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
JobResult jobResult = job.getResult();
assertThat(jobResult.getFailedCount()).isEqualTo(1);
assertThat(((DummyTaskResult) jobResult.getResults().get(0)).getFailure().getError()).isEqualTo("Timeout after 2000 ms"); // last error
});
}
@Test
public void testCancelJob_whileRunning() throws Exception {
int tasksCount = 100;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(100)
.build()).getId();
Thread.sleep(500);
cancelJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount);
});
}
@Test
public void testCancelJob_whileTaskRunning() throws Exception {
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(1)
.taskProcessingTimeMs(TimeUnit.HOURS.toMillis(1))
.taskProcessingTimeoutMs(TimeUnit.HOURS.toMillis(1))
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(taskProcessor.getCurrentTasks()).isNotEmpty();
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING);
assertThat(job.getResult().getTotalCount()).isEqualTo(1);
assertThat(job.getResult().getCompletedCount()).isZero();
});
cancelJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED);
assertThat(job.getResult().getTotalCount()).isEqualTo(1);
assertThat(job.getResult().getDiscardedCount()).isEqualTo(1);
assertThat(job.getResult().getFailedCount()).isZero();
});
}
@Test
public void testCancelJob_simulateTaskProcessorRestart() throws Exception {
int tasksCount = 10;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(500)
.build()).getId();
// simulate cancelled jobs are forgotten
AtomicInteger cancellationRenotifyAttempt = new AtomicInteger(0);
doAnswer(inv -> {
if (cancellationRenotifyAttempt.incrementAndGet() >= 5) {
inv.callRealMethod();
}
return null;
}).when(taskProcessor).addToDiscarded(any()); // ignoring cancellation event,
cancelJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount);
});
}
@Test
public void whenTenantIsDeleted_thenCancelAllTheJobs() throws Exception {
loginSysAdmin();
createDifferentTenant();
this.tenantId = this.differentTenantId;
submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(1000)
.taskProcessingTimeMs(500)
.build());
Thread.sleep(2000);
deleteDifferentTenant();
Mockito.reset(jobStatsService);
Thread.sleep(3000);
verify(jobStatsService, never()).reportTaskResult(any(), any(), any());
assertThat(jobDao.findByTenantIdAndFilter(tenantId, JobFilter.builder().build(), new PageLink(100)).getData()).isEmpty();
}
@Test
public void testSubmitMultipleJobs() throws Exception {
int tasksCount = 3;
int jobsCount = 3;
for (int i = 1; i <= jobsCount; i++) {
submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(1000)
.build(), "test-job-" + i);
}
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
List<Job> jobs = findJobs(List.of(JobType.DUMMY), List.of(jobEntity.getUuidId()));
assertThat(jobs).hasSize(jobsCount);
Job firstJob = jobs.get(2); // ordered by createdTime descending
assertThat(firstJob.getStatus()).isEqualTo(JobStatus.RUNNING);
Job secondJob = jobs.get(1);
assertThat(secondJob.getStatus()).isEqualTo(JobStatus.QUEUED);
Job thirdJob = jobs.get(0);
assertThat(thirdJob.getStatus()).isEqualTo(JobStatus.QUEUED);
});
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
List<Job> jobs = findJobs();
for (Job job : jobs) {
assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED);
assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
assertThat(job.getEntityId()).isEqualTo(jobEntity.getId());
assertThat(job.getEntityName()).isEqualTo(jobEntity.getName());
}
});
doDelete("/api/device/" + jobEntity.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(findJobs(List.of(JobType.DUMMY), List.of(jobEntity.getUuidId()))).isEmpty();
});
}
@Test
public void testCancelQueuedJob() throws Exception {
int tasksCount = 3;
int jobsCount = 3;
List<JobId> jobIds = new ArrayList<>();
for (int i = 1; i <= jobsCount; i++) {
Job job = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(1000)
.build(), "test-job-" + i);
jobIds.add(job.getId());
}
for (int i = 1; i < jobIds.size(); i++) {
cancelJob(jobIds.get(i));
}
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
List<Job> jobs = findJobs();
Job firstJob = jobs.get(2);
assertThat(firstJob.getStatus()).isEqualTo(JobStatus.COMPLETED);
assertThat(firstJob.getResult().getSuccessfulCount()).isEqualTo(tasksCount);
assertThat(firstJob.getResult().getTotalCount()).isEqualTo(tasksCount);
Job secondJob = jobs.get(1);
assertThat(secondJob.getStatus()).isEqualTo(JobStatus.CANCELLED);
assertThat(secondJob.getResult().getCompletedCount()).isZero();
Job thirdJob = jobs.get(0);
assertThat(thirdJob.getStatus()).isEqualTo(JobStatus.CANCELLED);
assertThat(thirdJob.getResult().getCompletedCount()).isZero();
});
}
@Test
public void testSubmitJob_generalError() {
int submittedTasks = 100;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.generalError("Some error while submitting tasks")
.submittedTasksBeforeGeneralError(submittedTasks)
.taskProcessingTimeMs(10)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, submittedTasks);
assertThat(job.getResult().getDiscardedCount()).isZero();
assertThat(job.getResult().getTotalCount()).isNull();
});
}
@Test
public void testSubmitJob_immediateGeneralError() {
JobId jobId = submitJob(DummyJobConfiguration.builder()
.generalError("Some error while submitting tasks")
.submittedTasksBeforeGeneralError(0)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
assertThat(job.getResult().getSuccessfulCount()).isZero();
assertThat(job.getResult().getDiscardedCount()).isZero();
assertThat(job.getResult().getFailedCount()).isZero();
assertThat(job.getResult().getTotalCount()).isNull();
});
}
@Test
public void testReprocessJob_generalError() throws Exception {
int submittedTasks = 100;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.generalError("Some error while submitting tasks")
.submittedTasksBeforeGeneralError(submittedTasks)
.taskProcessingTimeMs(10)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
assertThat(job.getResult().getGeneralError()).isEqualTo("Some error while submitting tasks");
});
Job savedJob = jobDao.findById(tenantId, jobId.getId());
DummyJobConfiguration configuration = savedJob.getConfiguration();
configuration.setGeneralError(null);
configuration.setSuccessfulTasksCount(submittedTasks);
jobDao.save(tenantId, savedJob);
reprocessJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED);
assertThat(job.getResult().getGeneralError()).isNull();
assertThat(job.getResult().getSuccessfulCount()).isEqualTo(submittedTasks);
assertThat(job.getResult().getTotalCount()).isEqualTo(submittedTasks);
assertThat(job.getResult().getFailedCount()).isZero();
assertThat(job.getResult().getDiscardedCount()).isZero();
});
}
@Test
public void testReprocessJob() throws Exception {
int successfulTasks = 3;
int failedTasks = 2;
int totalTasksCount = successfulTasks + failedTasks;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(successfulTasks)
.failedTasksCount(failedTasks)
.errors(List.of("error"))
.taskProcessingTimeMs(100)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
JobResult jobResult = job.getResult();
assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks);
assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks);
List<DummyTaskFailure> failures = getFailures(jobResult);
for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) {
DummyTaskFailure failure = failures.get(i);
assertThat(failure.getNumber()).isEqualTo(taskNumber);
assertThat(failure.getError()).isEqualTo("error");
}
});
reprocessJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED);
assertThat(job.getResult().getSuccessfulCount()).isEqualTo(totalTasksCount);
assertThat(job.getResult().getFailedCount()).isZero();
assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount);
assertThat(job.getResult().getResults()).isEmpty();
assertThat(job.getConfiguration().getToReprocess()).isNullOrEmpty();
});
}
@Test
public void testReprocessJob_somePermanentlyFailed() throws Exception {
int successfulTasks = 3;
int failedTasks = 2;
int permanentlyFailedTasks = 1;
int totalTasksCount = successfulTasks + failedTasks + permanentlyFailedTasks;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(successfulTasks)
.failedTasksCount(failedTasks)
.permanentlyFailedTasksCount(permanentlyFailedTasks)
.errors(List.of("error"))
.taskProcessingTimeMs(100)
.build()).getId();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
JobResult jobResult = job.getResult();
assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks);
assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks + permanentlyFailedTasks);
assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount);
List<DummyTaskFailure> failures = getFailures(jobResult);
for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) {
DummyTaskFailure failure = failures.get(i);
assertThat(failure.getNumber()).isEqualTo(taskNumber);
assertThat(failure.getError()).isEqualTo("error");
}
});
reprocessJob(jobId);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.FAILED);
JobResult jobResult = job.getResult();
assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks + failedTasks);
assertThat(jobResult.getFailedCount()).isEqualTo(permanentlyFailedTasks);
assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount);
List<DummyTaskFailure> failures = getFailures(jobResult);
for (int i = 0, taskNumber = successfulTasks + failedTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) {
DummyTaskFailure failure = failures.get(i);
assertThat(failure.getNumber()).isEqualTo(taskNumber);
assertThat(failure.getError()).isEqualTo("error");
assertThat(failure.isFailAlways()).isTrue();
}
});
}
private Job submitJob(DummyJobConfiguration configuration) {
return submitJob(configuration, "test-job");
}
@SneakyThrows
private Job submitJob(DummyJobConfiguration configuration, String key) {
return jobManager.submitJob(Job.builder()
.tenantId(tenantId)
.type(JobType.DUMMY)
.key(key)
.entityId(jobEntity.getId())
.configuration(configuration)
.build()).get();
}
private List<DummyTaskFailure> getFailures(JobResult jobResult) {
return jobResult.getResults().stream()
.map(taskResult -> ((DummyTaskResult) taskResult).getFailure())
.sorted(Comparator.comparingInt(DummyTaskFailure::getNumber))
.toList();
}
}

43
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.job;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
@TestPropertySource(properties = {
"queue.tasks.stats.processing_interval=0",
"queue.tasks.partitioning_strategy=entity",
"queue.tasks.partitions_per_type=DUMMY:100;DUMMY:50"
})
public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest {
/*
* Some tests are overridden because they are based on
* tenant partitioning strategy (subsequent tasks processing within a tenant)
* */
@Override
public void testCancelJob_simulateTaskProcessorRestart() throws Exception {
}
@Override
public void testSubmitJob_generalError() {
}
}

23
application/src/test/java/org/thingsboard/server/service/job/TestTaskProcessor.java

@ -0,0 +1,23 @@
/**
* 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.job;
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.job.task.DummyTaskProcessor;
@Component
public class TestTaskProcessor extends DummyTaskProcessor {
}

19
application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java

@ -52,7 +52,7 @@ public class RateLimitServiceTest {
public void beforeEach() {
tenantProfileCache = Mockito.mock(DefaultTbTenantProfileCache.class);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
tenantId = new TenantId(UUID.randomUUID());
tenantId = TenantId.fromUUID(UUID.randomUUID());
}
@Test
@ -67,7 +67,10 @@ public class RateLimitServiceTest {
profileConfiguration.setTenantServerRestLimitsConfiguration(rateLimit);
profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit);
profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit);
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit);
profileConfiguration.setCassandraReadQueryTenantCoreRateLimits(rateLimit);
profileConfiguration.setCassandraWriteQueryTenantCoreRateLimits(rateLimit);
profileConfiguration.setCassandraReadQueryTenantRuleEngineRateLimits(rateLimit);
profileConfiguration.setCassandraWriteQueryTenantRuleEngineRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit);
profileConfiguration.setEdgeUplinkMessagesRateLimits(rateLimit);
@ -79,7 +82,10 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS,
LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES,
LimitedApi.CASSANDRA_READ_QUERIES_CORE,
LimitedApi.CASSANDRA_WRITE_QUERIES_CORE,
LimitedApi.CASSANDRA_READ_QUERIES_RULE_ENGINE,
LimitedApi.CASSANDRA_WRITE_QUERIES_RULE_ENGINE,
LimitedApi.EDGE_EVENTS,
LimitedApi.EDGE_EVENTS_PER_EDGE,
LimitedApi.EDGE_UPLINK_MESSAGES,
@ -88,6 +94,13 @@ public class RateLimitServiceTest {
testRateLimits(limitedApi, max, tenantId);
}
for (LimitedApi limitedApi : List.of(
LimitedApi.CASSANDRA_READ_QUERIES_MONOLITH,
LimitedApi.CASSANDRA_WRITE_QUERIES_MONOLITH
)) {
testRateLimits(limitedApi, max * 2, tenantId);
}
CustomerId customerId = new CustomerId(UUID.randomUUID());
testRateLimits(LimitedApi.REST_REQUESTS_PER_CUSTOMER, max, customerId);

4
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -21,7 +21,6 @@ import org.junit.After;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.util.Pair;
import org.springframework.jdbc.core.JdbcTemplate;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.User;
@ -89,8 +88,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected SqlPartitioningRepository partitioningRepository;
@Autowired
protected DefaultNotifications defaultNotifications;
@Autowired
private JdbcTemplate jdbcTemplate;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@ -101,7 +98,6 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID);
notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
jdbcTemplate.execute("TRUNCATE TABLE notification");
partitioningRepository.cleanupPartitionsCache("notification", Long.MAX_VALUE, 0);
notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID);
}

65
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -68,6 +68,8 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
@ -78,6 +80,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Entit
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
@ -91,6 +94,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -98,8 +102,10 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService;
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.system.DefaultSystemInfoService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -134,6 +140,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean
private AlarmSubscriptionService alarmSubscriptionService;
@Autowired
private DefaultSystemInfoService systemInfoService;
@Autowired
private NotificationRequestService notificationRequestService;
@Autowired
private RateLimitService rateLimitService;
@ -254,7 +262,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(info.getAlarmStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
clients.values().forEach(wsClient -> wsClient.registerWaitForUpdate());
clients.values().forEach(TbTestWebSocketClient::registerWaitForUpdate);
alarmSubscriptionService.acknowledgeAlarm(tenantId, alarm.getId(), System.currentTimeMillis());
AlarmStatus expectedStatus = AlarmStatus.ACTIVE_ACK;
AlarmSeverity expectedSeverity = AlarmSeverity.CRITICAL;
@ -638,7 +646,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
rule = saveNotificationRule(rule);
NotificationRuleInfo ruleInfo = findNotificationRules().getData().get(0);
assertThat(ruleInfo.getId()).isEqualTo(ruleInfo.getId());
assertThat(ruleInfo.getId()).isEqualTo(rule.getId());
assertThat(ruleInfo.getTemplateName()).isEqualTo(template.getName());
assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods);
}
@ -780,6 +788,59 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
}
@Test
public void testNotificationRuleProcessing_resourcesShortage() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
}
@Test
public void testNotificationsDeduplication_resourcesShortage() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
assertThat(getMyNotifications(false, 100)).size().isZero();
for (int i = 0; i < 10; i++) {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM)
.usage(15L)
.build());
TimeUnit.MILLISECONDS.sleep(300);
}
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
// deduplication is 1 minute, no new message is exp
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM)
.usage(5L)
.build());
await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
}
@Test
public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();

2
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java

@ -238,7 +238,7 @@ public class TbRuleEngineStrategyTest {
.map(this::toProto)
.toList();
consumerManager.processMsgs(protoMsgs, consumer, queue);
consumerManager.processMsgs(protoMsgs, consumer, queueKey, queue);
processingData.forEach(data -> {
verify(actorContext, times(data.attempts)).tell(argThat(msg ->

79
application/src/test/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionServiceTest.java

@ -32,11 +32,16 @@ import org.thingsboard.server.cache.limits.RateLimitService;
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.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.HashMap;
@ -44,8 +49,13 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@ -54,11 +64,17 @@ import static org.mockito.Mockito.when;
public class DefaultTbLocalSubscriptionServiceTest {
ListAppender<ILoggingEvent> testLogAppender;
TbLocalSubscriptionService subscriptionService;
private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
private final DeviceId deviceId = new DeviceId(UUID.randomUUID());
private ListAppender<ILoggingEvent> testLogAppender;
private TbLocalSubscriptionService subscriptionService;
private ListeningExecutorService executorService;
@BeforeEach
public void setUp() throws Exception {
this.executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class);
testLogAppender = new ListAppender<>();
testLogAppender.start();
@ -69,7 +85,9 @@ public class DefaultTbLocalSubscriptionServiceTest {
PartitionService partitionService = mock();
when(partitionService.resolve(any(), any(), any())).thenReturn(TopicPartitionInfo.builder().build());
subscriptionService = new DefaultTbLocalSubscriptionService(mock(), mock(), mock(), partitionService, mock(), mock(), mock(), rateLimitService);
ReflectionTestUtils.setField(subscriptionService, "serviceId", "serviceId");
ReflectionTestUtils.setField(subscriptionService, "subscriptionUpdateExecutor", executorService);
}
@AfterEach
@ -79,27 +97,22 @@ public class DefaultTbLocalSubscriptionServiceTest {
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class);
logger.detachAppender(testLogAppender);
}
if (executorService != null) {
executorService.shutdownNow();
}
}
@Test
public void addSubscriptionConcurrentModificationTest() throws Exception {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
TenantId tenantId = new TenantId(UUID.randomUUID());
DeviceId deviceId = new DeviceId(UUID.randomUUID());
WebSocketSessionRef sessionRef = mock();
ReflectionTestUtils.setField(subscriptionService, "subscriptionUpdateExecutor", executorService);
List<ListenableFuture<?>> futures = new ArrayList<>();
try {
subscriptionService.onCoreStartupMsg(TransportProtos.CoreStartupMsg.newBuilder().addAllPartitions(List.of(0)).getDefaultInstanceForType());
for (int i = 0; i < 50; i++) {
futures.add(executorService.submit(() -> subscriptionService.addSubscription(createSubscription(tenantId, deviceId), sessionRef)));
}
Futures.allAsList(futures).get();
} finally {
executorService.shutdownNow();
subscriptionService.onCoreStartupMsg(TransportProtos.CoreStartupMsg.newBuilder().addAllPartitions(List.of(0)).getDefaultInstanceForType());
for (int i = 0; i < 50; i++) {
futures.add(executorService.submit(() -> subscriptionService.addSubscription(createSubscription(tenantId, deviceId), sessionRef)));
}
Futures.allAsList(futures).get();
List<ILoggingEvent> logs = testLogAppender.list;
boolean exceptionLogged = logs.stream()
@ -123,4 +136,42 @@ public class DefaultTbLocalSubscriptionServiceTest {
.keyStates(keys)
.build();
}
@Test
public void updateOldTelemetryTest() {
WebSocketSessionRef sessionRef = mock();
String key = "temperature";
long ts = 1237465456L;
Map<String, Long> keyStates = new HashMap<>();
keyStates.put(key, ts);
AtomicReference<TelemetrySubscriptionUpdate> capturedUpdate = new AtomicReference<>();
TbTimeSeriesSubscription tsSubscription = TbTimeSeriesSubscription.builder()
.tenantId(tenantId)
.entityId(deviceId)
.subscriptionId(2)
.sessionId(RandomStringUtils.randomAlphanumeric(5))
.keyStates(keyStates)
.allKeys(true)
.latestValues(true)
.updateProcessor((subscription, update) -> capturedUpdate.set(update))
.build();
subscriptionService.addSubscription(tsSubscription, sessionRef);
// Send telemetry with ts == stateTs
TsKvEntry kv = new BasicTsKvEntry(ts, new StringDataEntry(key, "42"));
subscriptionService.onTimeSeriesUpdate(deviceId, List.of(kv), mock(TbCallback.class));
await().atMost(5, TimeUnit.SECONDS)
.pollInterval(5, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
TelemetrySubscriptionUpdate update = capturedUpdate.get();
assertNotNull(update);
assertTrue(update.getLatestValues().containsKey(key));
});
}
}

130
application/src/test/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtxTest.java

@ -0,0 +1,130 @@
/**
* 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.subscription;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbEntityDataSubCtxTest {
private final DeviceId deviceId = new DeviceId(UUID.randomUUID());
private final Integer cmdId = RandomUtils.nextInt();
private final Integer subscriptionId = RandomUtils.nextInt();
private final String serviceId = RandomStringUtils.randomAlphanumeric(10);
private final String sessionId = RandomStringUtils.randomAlphanumeric(10);
private final int maxEntitiesPerDataSubscription = 100;
private TbEntityDataSubCtx subCtx;
@Mock
private WebSocketService webSocketService;
@Mock
private WebSocketSessionRef webSocketSessionRef;
@BeforeEach
public void setUp() {
when(webSocketSessionRef.getSessionId()).thenReturn(sessionId);
subCtx = new TbEntityDataSubCtx(serviceId, webSocketService, mock(), mock(), mock(), mock(), webSocketSessionRef, cmdId, maxEntitiesPerDataSubscription);
Map<Integer, EntityId> subToEntityIdMap = new HashMap<>();
subToEntityIdMap.put(subscriptionId, deviceId);
ReflectionTestUtils.setField(subCtx, "subToEntityIdMap", subToEntityIdMap);
long now = System.currentTimeMillis();
long oldTs = now - 1_000_000;
Map<String, TsValue> latestCtxValues = new HashMap<>();
latestCtxValues.put("key", new TsValue(oldTs, "15"));
Map<EntityKeyType, Map<String, TsValue>> latest = new HashMap<>();
latest.put(EntityKeyType.TIME_SERIES, latestCtxValues);
EntityData entityData = new EntityData();
entityData.setEntityId(deviceId);
entityData.setLatest(latest);
PageData<EntityData> data = new PageData<>(List.of(entityData), 1, 1, true);
ReflectionTestUtils.setField(subCtx, "data", data);
}
@Test
public void testSendLatestWsMsg() {
long ts = System.currentTimeMillis();
List<TsKvEntry> telemetry = List.of(
new BasicTsKvEntry(ts - 50000, new LongDataEntry("key", 42L), 34L),
new BasicTsKvEntry(ts - 20000, new LongDataEntry("key", 17L), 78L)
);
TelemetrySubscriptionUpdate subUpdate = new TelemetrySubscriptionUpdate(subscriptionId, telemetry);
subCtx.sendWsMsg(sessionId, subUpdate, EntityKeyType.TIME_SERIES, true);
Map<EntityKeyType, Map<String, TsValue>> expectedLatest = new HashMap<>();
Map<String, TsValue> expectedLatestCtxValues = new HashMap<>();
expectedLatestCtxValues.put("key", new TsValue(ts - 20000, "17")); // use latest telemetry
expectedLatest.put(EntityKeyType.TIME_SERIES, expectedLatestCtxValues);
EntityData expectedEntityData = new EntityData();
expectedEntityData.setEntityId(deviceId);
expectedEntityData.setLatest(expectedLatest);
List<EntityData> expected = List.of(expectedEntityData);
ArgumentCaptor<CmdUpdate> cmdUpdateCaptor = ArgumentCaptor.forClass(CmdUpdate.class);
then(webSocketService).should().sendUpdate(eq(sessionId), cmdUpdateCaptor.capture());
CmdUpdate cmdUpdate = cmdUpdateCaptor.getValue();
assertThat(cmdUpdate).isInstanceOf(EntityDataUpdate.class);
EntityDataUpdate entityDataUpdate = (EntityDataUpdate) cmdUpdate;
assertThat(entityDataUpdate.getCmdId()).isEqualTo(cmdId);
assertThat(entityDataUpdate.getData()).isNull();
assertThat(entityDataUpdate.getUpdate()).isEqualTo(expected);
assertThat(entityDataUpdate.getAllowedEntities()).isEqualTo(maxEntitiesPerDataSubscription);
}
}

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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

@ -87,6 +87,8 @@ public interface TbClusterService extends TbQueueClusterService {
void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
void broadcast(ComponentLifecycleMsg componentLifecycleMsg);
void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback);
void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback);

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

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.queue;
public interface TbQueueCallback {
TbQueueCallback EMPTY = new TbQueueCallback() {
@ -34,4 +33,5 @@ public interface TbQueueCallback {
void onSuccess(TbQueueMsgMetadata metadata);
void onFailure(Throwable t);
}

3
common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueProducer.java

@ -19,11 +19,10 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
public interface TbQueueProducer<T extends TbQueueMsg> {
void init();
String getDefaultTopic();
void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback);
void stop();
}

6
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
@ -46,6 +46,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common.transport</groupId>
<artifactId>transport-api</artifactId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

5
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -25,7 +26,9 @@ import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public interface EntityService {
@ -37,6 +40,8 @@ public interface EntityService {
Optional<HasId<?>> fetchEntity(TenantId tenantId, EntityId entityId);
Map<EntityId, EntityInfo> fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set<EntityId> entityIds);
Optional<NameLabelAndCustomerDetails> fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId);
long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query);

48
common/dao-api/src/main/java/org/thingsboard/server/dao/job/JobService.java

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.job;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobFilter;
import org.thingsboard.server.common.data.job.JobStats;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.EntityDaoService;
public interface JobService extends EntityDaoService {
Job saveJob(TenantId tenantId, Job job);
Job findJobById(TenantId tenantId, JobId jobId);
void cancelJob(TenantId tenantId, JobId jobId);
void markAsFailed(TenantId tenantId, JobId jobId, String error);
void processStats(TenantId tenantId, JobId jobId, JobStats jobStats);
PageData<Job> findJobsByFilter(TenantId tenantId, JobFilter filter, PageLink pageLink);
Job findLatestJobByKey(TenantId tenantId, String key);
void deleteJob(TenantId tenantId, JobId jobId);
int deleteJobsByEntityId(TenantId tenantId, EntityId entityId);
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java

@ -49,7 +49,10 @@ public interface TenantService extends EntityDaoService {
List<TenantId> findTenantIdsByTenantProfileId(TenantProfileId tenantProfileId);
Tenant findTenantByName(String name);
void deleteTenants();
PageData<TenantId> findTenantsIds(PageLink pageLink);
}

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-RC</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

20
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
@ -63,7 +64,8 @@ public enum EntityType {
MOBILE_APP(37),
MOBILE_APP_BUNDLE(38),
CALCULATED_FIELD(39),
CALCULATED_FIELD_LINK(40);
CALCULATED_FIELD_LINK(40),
JOB(41);
@Getter
private final int protoNumber; // Corresponds to EntityTypeProto
@ -76,6 +78,15 @@ public enum EntityType {
public static final List<String> NORMAL_NAMES = EnumSet.allOf(EntityType.class).stream()
.map(EntityType::getNormalName).toList();
private static final EntityType[] BY_PROTO;
static {
BY_PROTO = new EntityType[Arrays.stream(values()).mapToInt(EntityType::getProtoNumber).max().orElse(0) + 1];
for (EntityType entityType : values()) {
BY_PROTO[entityType.getProtoNumber()] = entityType;
}
}
EntityType(int protoNumber) {
this.protoNumber = protoNumber;
this.tableName = name().toLowerCase();
@ -98,4 +109,11 @@ public enum EntityType {
return false;
}
public static EntityType forProtoNumber(int protoNumber) {
if (protoNumber < 0 || protoNumber >= BY_PROTO.length) {
throw new IllegalArgumentException("Invalid EntityType proto number " + protoNumber);
}
return BY_PROTO[protoNumber];
}
}

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

@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import java.util.UUID;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ -58,7 +60,7 @@ public class AttributeKv implements EdqsObject {
}
@Override
public String key() {
public String stringKey() {
return "a_" + entityId + "_" + scope + "_" + key;
}
@ -72,4 +74,6 @@ public class AttributeKv implements EdqsObject {
return ObjectType.ATTRIBUTE_KV;
}
public record Key(UUID entityId, AttributeScope scope, int key) implements EdqsObjectKey {}
}

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

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.ObjectType;
public interface EdqsObject {
@JsonIgnore
String key();
String stringKey();
@JsonIgnore
Long version();

18
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsObjectKey.java

@ -0,0 +1,18 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.edqs;
public interface EdqsObjectKey {}

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

@ -0,0 +1,75 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.edqs;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.BooleanUtils;
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class EdqsState {
private Boolean edqsReady;
@Setter
private EdqsSyncStatus syncStatus;
@Setter
private EdqsApiMode apiMode;
public boolean updateEdqsReady(boolean ready) {
boolean changed = BooleanUtils.toBooleanDefaultIfNull(this.edqsReady, false) != ready;
this.edqsReady = ready;
return changed;
}
@JsonIgnore
public boolean isApiReady() {
return edqsReady && syncStatus == EdqsSyncStatus.FINISHED;
}
@JsonIgnore
public boolean isApiEnabled() {
return apiMode != null && (apiMode == EdqsApiMode.ENABLED || apiMode == EdqsApiMode.AUTO_ENABLED);
}
@Override
public String toString() {
return '[' +
"EDQS ready: " + edqsReady +
", sync status: " + syncStatus +
", API mode: " + apiMode +
']';
}
public enum EdqsSyncStatus {
REQUESTED,
STARTED,
FINISHED,
FAILED
}
public enum EdqsApiMode {
ENABLED,
AUTO_ENABLED,
DISABLED,
AUTO_DISABLED
}
}

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

@ -51,7 +51,7 @@ public class Entity implements EdqsObject {
}
@Override
public String key() {
public String stringKey() {
return "e_" + fields.getId().toString();
}
@ -65,4 +65,6 @@ public class Entity implements EdqsObject {
return ObjectType.fromEntityType(type);
}
public record Key(UUID id) implements EdqsObjectKey {}
}

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

@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.UUID;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ -53,7 +55,8 @@ public class LatestTsKv implements EdqsObject {
this.version = version != null ? version : 0L;
}
public String key() {
@Override
public String stringKey() {
return "l_" + entityId + "_" + key;
}
@ -67,4 +70,6 @@ public class LatestTsKv implements EdqsObject {
return ObjectType.LATEST_TS_KV;
}
public record Key(UUID entityId, int key) implements EdqsObjectKey {}
}

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

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.edqs.EdqsState.EdqsSyncStatus;
@Data
@AllArgsConstructor
@ -29,4 +30,7 @@ public class ToCoreEdqsMsg {
private EdqsSyncRequest syncRequest;
private Boolean apiEnabled;
private EdqsSyncStatus syncStatus;
private Boolean edqsReady;
}

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

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.common.data.edqs;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -30,9 +29,4 @@ public class ToCoreEdqsRequest {
private EdqsSyncRequest syncRequest;
private Boolean apiEnabled;
@JsonIgnore
public ToCoreEdqsMsg toInternalMsg() {
return new ToCoreEdqsMsg(syncRequest, apiEnabled);
}
}

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

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.edqs.fields;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.UUID;
@ -36,7 +36,7 @@ public class AbstractEntityFields implements EntityFields {
this.id = id;
this.createdTime = createdTime;
this.tenantId = tenantId;
this.customerId = (customerId != null && customerId != CustomerId.NULL_UUID) ? customerId : null;
this.customerId = checkId(customerId);
this.name = name;
this.version = version;
}
@ -62,4 +62,13 @@ public class AbstractEntityFields implements EntityFields {
this(id, createdTime, tenantId, null, null, null);
}
protected UUID checkId(UUID id) {
return id == null || id.equals(EntityId.NULL_UUID) ? null : id;
}
@Override
public UUID getCustomerId() {
return checkId(customerId);
}
}

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

@ -29,9 +29,7 @@ public interface EntityFields {
Logger log = LoggerFactory.getLogger(EntityFields.class);
default UUID getId() {
return null;
}
UUID getId();
default UUID getTenantId() {
return null;
@ -147,6 +145,7 @@ public interface EntityFields {
default String getAsString(String key) {
return switch (key) {
case "id" -> getId().toString();
case "createdTime" -> Long.toString(getCreatedTime());
case "title" -> getName();
case "type" -> getType();

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java

@ -85,6 +85,10 @@ public class HousekeeperTask implements Serializable {
return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_CALCULATED_FIELDS);
}
public static HousekeeperTask deleteJobs(TenantId tenantId, EntityId entityId) {
return new HousekeeperTask(tenantId, entityId, HousekeeperTaskType.DELETE_JOBS);
}
@JsonIgnore
public String getDescription() {
return taskType.getDescription() + " for " + entityId.getEntityType().getNormalName().toLowerCase() + " " + entityId.getId();

3
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTaskType.java

@ -31,7 +31,8 @@ public enum HousekeeperTaskType {
UNASSIGN_ALARMS("alarms unassigning"),
DELETE_TENANT_ENTITIES("tenant entities deletion"),
DELETE_ENTITIES("entities deletion"),
DELETE_CALCULATED_FIELDS("calculated fields deletion");
DELETE_CALCULATED_FIELDS("calculated fields deletion"),
DELETE_JOBS("jobs deletion");
private final String description;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -117,6 +117,8 @@ public class EntityIdFactory {
return new CalculatedFieldId(uuid);
case CALCULATED_FIELD_LINK:
return new CalculatedFieldLinkId(uuid);
case JOB:
return new JobId(uuid);
}
throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}

38
common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
public class JobId extends UUIDBased implements EntityId {
@JsonCreator
public JobId(@JsonProperty("id") UUID id) {
super(id);
}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "JOB", allowableValues = "JOB")
@Override
public EntityType getEntityType() {
return EntityType.JOB;
}
}

51
common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.job;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString(callSuper = true)
public class DummyJobConfiguration extends JobConfiguration {
private long taskProcessingTimeMs;
private int successfulTasksCount;
private int failedTasksCount;
private int permanentlyFailedTasksCount;
private List<String> errors;
private int retries;
private long taskProcessingTimeoutMs;
private String generalError;
private int submittedTasksBeforeGeneralError;
@Override
public JobType getType() {
return JobType.DUMMY;
}
}

25
common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobResult.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.job;
public class DummyJobResult extends JobResult {
@Override
public JobType getJobType() {
return JobType.DUMMY;
}
}

85
common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.job;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Set;
import java.util.UUID;
@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Job extends BaseData<JobId> implements HasTenantId {
@NotNull
private TenantId tenantId;
@NotNull
private JobType type;
@NotBlank
private String key;
@NotNull
private EntityId entityId;
private String entityName; // read-only
@NotNull
private JobStatus status;
@NotNull
@Valid
private JobConfiguration configuration;
@NotNull
private JobResult result;
public static final Set<EntityType> SUPPORTED_ENTITY_TYPES = Set.of(
EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE
);
@Builder(toBuilder = true)
public Job(TenantId tenantId, JobType type, String key, EntityId entityId, JobConfiguration configuration) {
this.tenantId = tenantId;
this.type = type;
this.key = key;
this.entityId = entityId;
this.configuration = configuration;
this.configuration.setTasksKey(UUID.randomUUID().toString());
presetResult();
}
public void presetResult() {
this.result = switch (type) {
case DUMMY -> new DummyJobResult();
};
}
@SuppressWarnings("unchecked")
public <C extends JobConfiguration> C getConfiguration() {
return (C) configuration;
}
}

45
common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.job;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.thingsboard.server.common.data.job.task.TaskResult;
import java.io.Serializable;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@Type(name = "DUMMY", value = DummyJobConfiguration.class),
})
@Data
public abstract class JobConfiguration implements Serializable {
@NotBlank
private String tasksKey; // internal
private List<TaskResult> toReprocess; // internal
@JsonIgnore
public abstract JobType getType();
}

23
edqs/src/main/java/org/thingsboard/server/edqs/DummyQueueRoutingInfoService.java → common/data/src/main/java/org/thingsboard/server/common/data/job/JobFilter.java

@ -13,21 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edqs;
package org.thingsboard.server.common.data.job;
import org.springframework.stereotype.Service;
import org.thingsboard.server.queue.discovery.QueueRoutingInfo;
import org.thingsboard.server.queue.discovery.QueueRoutingInfoService;
import lombok.Builder;
import lombok.Data;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@Service
public class DummyQueueRoutingInfoService implements QueueRoutingInfoService {
@Data
@Builder
public class JobFilter {
@Override
public List<QueueRoutingInfo> getAllQueuesRoutingInfo() {
return Collections.emptyList();
}
private final List<JobType> types;
private final List<JobStatus> statuses;
private final List<UUID> entities;
private final Long startTime;
private final Long endTime;
}

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

Loading…
Cancel
Save