Browse Source

merge with master and minot refactoring

pull/10734/head
YevhenBondarenko 2 years ago
parent
commit
c1f59bc25b
  1. 9
      .github/ISSUE_TEMPLATE/---bug-report.md
  2. 5
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 5
      .github/ISSUE_TEMPLATE/question.md
  4. 1
      application/src/main/data/json/system/widget_bundles/cards.json
  5. 26
      application/src/main/data/json/system/widget_types/mobile_app_qr_code.json
  6. 15
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  7. 15
      application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
  8. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  9. 3
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  10. 2
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  11. 4
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  12. 6
      application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
  13. 2
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  14. 4
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  15. 188
      application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java
  16. 8
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  17. 2
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  18. 2
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  19. 10
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  20. 3
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  21. 12
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  22. 10
      application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
  23. 15
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  24. 30
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  25. 28
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java
  26. 66
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java
  27. 33
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java
  28. 25
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java
  29. 35
      application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java
  30. 17
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  31. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  32. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  33. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  34. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  35. 4
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  36. 12
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  37. 105
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  38. 39
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java
  39. 1
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  40. 49
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  41. 2
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
  42. 3
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
  43. 2
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
  44. 24
      application/src/main/resources/thingsboard.yml
  45. 6
      application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
  46. 95
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  47. 63
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  48. 253
      application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java
  49. 8
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  50. 1
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  51. 6
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  52. 2
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  53. 3
      application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java
  54. 5
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  55. 33
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  56. 3
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  57. 98
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteCborTest.java
  58. 27
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java
  59. 172
      application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java
  60. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  61. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  62. 2
      common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
  63. 2
      common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
  64. 2
      common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java
  65. 2
      common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java
  66. 37
      common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java
  67. 1
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  68. 38
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java
  69. 23
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java
  70. 24
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java
  71. 36
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java
  72. 45
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java
  73. 40
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java
  74. 2
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
  75. 1
      common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
  76. 7
      common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java
  77. 17
      common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java
  78. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
  79. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
  80. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  81. 2
      common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
  82. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  83. 4
      common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java
  84. 61
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  85. 6
      common/message/src/main/proto/tbmsg.proto
  86. 4
      common/proto/src/main/proto/queue.proto
  87. 74
      common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java
  88. 137
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  89. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
  90. 9
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java
  91. 45
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
  92. 16
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java
  93. 35
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  94. 4
      common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java
  95. 9
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  96. 51
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java
  97. 13
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  98. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
  99. 445
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  100. 103
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

9
.github/ISSUE_TEMPLATE/---bug-report.md

@ -12,8 +12,8 @@ A clear and concise description of what the bug is.
**Your Server Environment** **Your Server Environment**
<!-- 🔅🔅🔅🔅🔅🔅🔅 Choose one of the following or write your own 🔅🔅🔅🔅🔅🔅🔅--> <!-- 🔅🔅🔅🔅🔅🔅🔅 Choose one of the following or write your own 🔅🔅🔅🔅🔅🔅🔅-->
* [https://demo.thingsboard.io](demo.thingsboard.io) * [Live Demo](https://demo.thingsboard.io)
* [https://thingsboard.cloud](thingsboard.cloud) * [ThingsBoard Cloud](https://thingsboard.cloud)
* own setup * own setup
* Deployment: monolith or microservices * Deployment: monolith or microservices
* Deployment type: deb, rpm, exe, docker-compose, k8s, ami * Deployment type: deb, rpm, exe, docker-compose, k8s, ami
@ -60,3 +60,8 @@ If applicable, please add screenshots to help explain your problem.
**Additional context** **Additional context**
Please feel free to add any other context about the problem here. Please feel free to add any other context about the problem here.
________________________________________________________________
**Disclaimer**
We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel.

5
.github/ISSUE_TEMPLATE/feature_request.md

@ -18,3 +18,8 @@ A clear and concise description of any alternative solutions or features you've
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.
_____________________________________________________
**Disclaimer**
We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies)), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel.

5
.github/ISSUE_TEMPLATE/question.md

@ -23,3 +23,8 @@ Clear and concise details.
* OS: name and version * OS: name and version
* ThingsBoard: version * ThingsBoard: version
* Browser: name and version * Browser: name and version
___________________________________________________________
**Disclaimer**
We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel.

1
application/src/main/data/json/system/widget_bundles/cards.json

@ -17,6 +17,7 @@
"cards.label_widget", "cards.label_widget",
"cards.dashboard_state_widget", "cards.dashboard_state_widget",
"cards.qr_code", "cards.qr_code",
"mobile_app_qr_code",
"cards.attributes_card", "cards.attributes_card",
"cards.html_card", "cards.html_card",
"cards.html_value_card", "cards.html_value_card",

26
application/src/main/data/json/system/widget_types/mobile_app_qr_code.json

File diff suppressed because one or more lines are too long

15
application/src/main/data/upgrade/3.6.4/schema_update.sql

@ -145,3 +145,18 @@ DELETE FROM asset WHERE type='TbServiceQueue';
DELETE FROM asset_profile WHERE name ='TbServiceQueue'; DELETE FROM asset_profile WHERE name ='TbServiceQueue';
-- QUEUE STATS UPDATE END -- QUEUE STATS UPDATE END
-- MOBILE APP SETTINGS TABLE CREATE START
CREATE TABLE IF NOT EXISTS mobile_app_settings (
id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
use_default_app boolean,
android_config VARCHAR(1000),
ios_config VARCHAR(1000),
qr_code_config VARCHAR(100000),
CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id)
);
-- MOBILE APP SETTINGS TABLE CREATE END

15
application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java

@ -15,24 +15,32 @@
*/ */
package org.thingsboard.server; package org.thingsboard.server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.Ordered;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.thingsboard.server.queue.util.AfterStartUp;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@SpringBootConfiguration @SpringBootConfiguration
@EnableAsync @EnableAsync
@EnableScheduling @EnableScheduling
@ComponentScan({"org.thingsboard.server", "org.thingsboard.script"}) @ComponentScan({"org.thingsboard.server", "org.thingsboard.script"})
@Slf4j
public class ThingsboardServerApplication { public class ThingsboardServerApplication {
private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";
private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard"; private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard";
private static long startTs;
public static void main(String[] args) { public static void main(String[] args) {
startTs = System.currentTimeMillis();
SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args)); SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args));
} }
@ -45,4 +53,11 @@ public class ThingsboardServerApplication {
} }
return args; return args;
} }
@AfterStartUp(order = Ordered.LOWEST_PRECEDENCE)
public void afterStartUp() {
long startupTimeMs = System.currentTimeMillis() - startTs;
log.info("Started ThingsBoard in {} seconds", TimeUnit.MILLISECONDS.toSeconds(startupTimeMs));
}
} }

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

@ -636,6 +636,10 @@ public class ActorSystemContext {
return partitionService.resolve(serviceType, queueName, tenantId, entityId); return partitionService.resolve(serviceType, queueName, tenantId, entityId);
} }
public TopicPartitionInfo resolve(TenantId tenantId, EntityId entityId, TbMsg msg) {
return partitionService.resolve(ServiceType.TB_RULE_ENGINE, msg.getQueueName(), tenantId, entityId, msg.getPartition());
}
public String getServiceId() { public String getServiceId() {
return serviceInfoProvider.getServiceId(); return serviceInfoProvider.getServiceId();
} }

3
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -42,7 +42,6 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
@ -295,7 +294,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
try { try {
checkComponentStateActive(msg); checkComponentStateActive(msg);
EntityId entityId = msg.getOriginator(); EntityId entityId = msg.getOriginator();
TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, msg.getQueueName(), tenantId, entityId); TopicPartitionInfo tpi = systemContext.resolve(tenantId, entityId, msg);
List<RuleNodeRelation> ruleNodeRelations = nodeRoutes.get(originatorNodeId); List<RuleNodeRelation> ruleNodeRelations = nodeRoutes.get(originatorNodeId);
if (ruleNodeRelations == null) { // When unchecked, this will cause NullPointerException when rule node doesn't exist anymore if (ruleNodeRelations == null) { // When unchecked, this will cause NullPointerException when rule node doesn't exist anymore

2
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -76,7 +76,7 @@ public class ThingsboardSecurityConfiguration {
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"}; protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String WS_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";

4
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -256,7 +256,9 @@ public class AuthController extends BaseController {
} }
} }
return tokenFactory.createTokenPair(securityUser); var tokenPair = tokenFactory.createTokenPair(securityUser);
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null);
return tokenPair;
} }
@ApiOperation(value = "Reset password (resetPassword)", @ApiOperation(value = "Reset password (resetPassword)",

6
application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java

@ -68,9 +68,9 @@ public class ComponentDescriptorController extends BaseController {
@RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public List<ComponentDescriptor> getComponentDescriptorsByType( public List<ComponentDescriptor> getComponentDescriptorsByType(
@Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", requiredMode = Schema.RequiredMode.REQUIRED)) @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = {"ENRICHMENT", "FILTER,TRANSFORMATION", "ACTION,EXTERNAL"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("componentType") String strComponentType, @PathVariable("componentType") String strComponentType,
@Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE")) @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"}))
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
checkParameter("componentType", strComponentType); checkParameter("componentType", strComponentType);
return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType));
@ -85,7 +85,7 @@ public class ComponentDescriptorController extends BaseController {
public List<ComponentDescriptor> getComponentDescriptorsByTypes( public List<ComponentDescriptor> getComponentDescriptorsByTypes(
@Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true) @Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true)
@RequestParam("componentTypes") String[] strComponentTypes, @RequestParam("componentTypes") String[] strComponentTypes,
@Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE")) @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"}))
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
checkArrayParameter("componentTypes", strComponentTypes); checkArrayParameter("componentTypes", strComponentTypes);
Set<ComponentType> componentTypes = new HashSet<>(); Set<ComponentType> componentTypes = new HashSet<>();

2
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -164,7 +164,7 @@ public class CustomerController extends BaseController {
@RequestParam int page, @RequestParam int page,
@Parameter(description = CUSTOMER_TEXT_SEARCH_DESCRIPTION) @Parameter(description = CUSTOMER_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "email", "country, city"})) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "email", "country", "city"}))
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {

4
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -229,7 +229,7 @@ public class EntityViewController extends BaseController {
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name, type"})) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "type"}))
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
@ -293,7 +293,7 @@ public class EntityViewController extends BaseController {
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name, type"})) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "type"}))
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {

188
application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java

@ -0,0 +1,188 @@
/**
* Copyright © 2016-2024 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 com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.mobile.secret.MobileAppSecretService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
@RequiredArgsConstructor
@RestController
@TbCoreComponent
public class MobileApplicationController extends BaseController {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
public static final String ASSET_LINKS_PATTERN = "[{\n" +
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" +
" \"target\": {\n" +
" \"namespace\": \"android_app\",\n" +
" \"package_name\": \"%s\",\n" +
" \"sha256_cert_fingerprints\":\n" +
" [\"%s\"]\n" +
" }\n" +
"}]";
public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" +
" \"applinks\": {\n" +
" \"apps\": [],\n" +
" \"details\": [\n" +
" {\n" +
" \"appID\": \"%s\",\n" +
" \"paths\": [ \"/api/noauth/qr\" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app";
public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695";
public static final String SECRET = "secret";
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key";
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io";
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s";
private final SystemSecurityService systemSecurityService;
private final MobileAppSecretService mobileAppSecretService;
private final MobileAppSettingsService mobileAppSettingsService;
@ApiOperation(value = "Get associated android applications (getAssetLinks)")
@GetMapping(value = "/.well-known/assetlinks.json")
public ResponseEntity<JsonNode> getAssetLinks() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig();
if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)")
@GetMapping(value = "/.well-known/apple-app-site-association")
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() {
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
IosConfig iosConfig = mobileAppSettings.getIosConfig();
if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) {
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId())));
} else {
return ResponseEntity.notFound().build();
}
}
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)",
notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping(value = "/api/mobile/app/settings")
public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration")
@RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE);
mobileAppSettings.setTenantId(getTenantId());
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings);
}
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/app/settings")
public MobileAppSettings getMobileAppSettings() throws ThingsboardException {
SecurityUser currentUser = getCurrentUser();
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ);
return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
}
@ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)",
notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/deepLink", produces = "text/plain")
public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException {
String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser());
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request);
String platformDomain = new URI(baseUrl).getHost();
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID);
String appDomain;
if (!mobileAppSettings.isUseDefaultApp()) {
appDomain = platformDomain;
} else {
appDomain = DEFAULT_APP_DOMAIN;
}
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl);
if (!appDomain.equals(platformDomain)) {
deepLink = deepLink + "&host=" + baseUrl;
}
return "\"" + deepLink + "\"";
}
@ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)",
notes = "Returns the token of the User based on the provided secret key.")
@GetMapping(value = "/api/noauth/qr/{secret}")
public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION)
@PathVariable(SECRET) String secret) throws ThingsboardException {
checkParameter(SECRET, secret);
return mobileAppSecretService.getJwtPair(secret);
}
@GetMapping(value = "/api/noauth/qr")
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) {
if (userAgent.contains("Android")) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", ANDROID_APPLICATION_STORE_LINK)
.build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", APPLE_APPLICATION_STORE_LINK)
.build();
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.build();
}
}
}

8
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -106,8 +106,6 @@ public class NotificationController extends BaseController {
private final NotificationCenter notificationCenter; private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService; private final NotificationSettingsService notificationSettingsService;
private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP";
@ApiOperation(value = "Get notifications (getNotifications)", @ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE + notes = "Returns the page of notifications for current user." + NEW_LINE +
PAGE_DATA_PARAMETERS + PAGE_DATA_PARAMETERS +
@ -175,7 +173,7 @@ public class NotificationController extends BaseController {
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@Parameter(description = "To search for unread notifications only") @Parameter(description = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly, @RequestParam(defaultValue = "false") boolean unreadOnly,
@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"}))
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions // no permissions
@ -188,7 +186,7 @@ public class NotificationController extends BaseController {
AVAILABLE_FOR_ANY_AUTHORIZED_USER) AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@GetMapping("/notifications/unread/count") @GetMapping("/notifications/unread/count")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"}))
@RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod, @RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId()); return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId());
@ -211,7 +209,7 @@ public class NotificationController extends BaseController {
AVAILABLE_FOR_ANY_AUTHORIZED_USER) AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notifications/read") @PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"}))
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
// no permissions // no permissions

2
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -68,7 +68,7 @@ public class OAuth2Controller extends BaseController {
@Parameter(description = "Platform type to search OAuth2 clients for which " + @Parameter(description = "Platform type to search OAuth2 clients for which " +
"the usage with this platform type is allowed in the settings. " + "the usage with this platform type is allowed in the settings. " +
"If platform type is not one of allowable values - it will just be ignored", "If platform type is not one of allowable values - it will just be ignored",
schema = @Schema(allowableValues = "WEB, ANDROID, IOS")) schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"}))
@RequestParam(required = false) String platform) throws ThingsboardException { @RequestParam(required = false) String platform) throws ThingsboardException {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort());

2
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -198,7 +198,7 @@ public class OtaPackageController extends BaseController {
@ResponseBody @ResponseBody
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId, @PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = "FIRMWARE, SOFTWARE")) @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@PathVariable("type") String strType, @PathVariable("type") String strType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,

10
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -34,10 +34,13 @@ import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.MobileAppSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
@ -45,6 +48,7 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Hidden @Hidden
@ -72,6 +76,9 @@ public class SystemInfoController extends BaseController {
@Autowired @Autowired
private EntitiesVersionControlService versionControlService; private EntitiesVersionControlService versionControlService;
@Autowired
private MobileAppSettingsService mobileAppSettingsService;
@PostConstruct @PostConstruct
public void init() { public void init() {
JsonNode info = buildInfoObject(); JsonNode info = buildInfoObject();
@ -135,6 +142,9 @@ public class SystemInfoController extends BaseController {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
} }
systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID))
.map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)
.orElse(false));
return systemParams; return systemParams;
} }

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

@ -138,6 +138,9 @@ public class TenantProfileController extends BaseController {
" \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" + " \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" + " \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" + " \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportGatewayTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
" \"maxTransportMessages\": 10000000,\n" + " \"maxTransportMessages\": 10000000,\n" +
" \"maxTransportDataPoints\": 10000000,\n" + " \"maxTransportDataPoints\": 10000000,\n" +
" \"maxREExecutions\": 4000000,\n" + " \"maxREExecutions\": 4000000,\n" +

12
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -539,6 +539,18 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
@Override
public boolean isOpen(String externalId) {
String internalId = externalSessionMap.get(externalId);
if (internalId != null) {
SessionMetaData sessionMd = getSessionMd(internalId);
if (sessionMd != null) {
return sessionMd.session.isOpen();
}
}
return false;
}
private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws IOException { private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws IOException {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) { if (tenantProfileConfiguration == null) {

10
application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java

@ -199,6 +199,16 @@ public abstract class BaseApiUsageState {
return getApiUsageState().getEntityId(); return getApiUsageState().getEntityId();
} }
@Override
public String toString() {
return "BaseApiUsageState{" +
"apiUsageState=" + apiUsageState +
", currentCycleTs=" + currentCycleTs +
", nextCycleTs=" + nextCycleTs +
", currentHourTs=" + currentHourTs +
'}';
}
@Data @Data
@Builder @Builder
public static class StatsCalculationResult { public static class StatsCalculationResult {

15
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -17,6 +17,8 @@ package org.thingsboard.server.service.apiusage;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@ -67,8 +69,6 @@ import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.partition.AbstractPartitionBasedService; import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
import org.thingsboard.server.service.telemetry.InternalTelemetryService; import org.thingsboard.server.service.telemetry.InternalTelemetryService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -184,6 +184,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
if (newHourTs != hourTs) { if (newHourTs != hourTs) {
usageState.setHour(newHourTs); usageState.setHour(newHourTs);
} }
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Processing usage stats from {} (currentCycleTs={}, currentHourTs={}): {}", tenantId, ownerId, serviceId, ts, newHourTs, values);
}
updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length);
Set<ApiFeature> apiFeatures = new HashSet<>(); Set<ApiFeature> apiFeatures = new HashSet<>();
for (UsageStatsKVProto statsItem : values) { for (UsageStatsKVProto statsItem : values) {
@ -210,6 +213,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
} finally { } finally {
updateLock.unlock(); updateLock.unlock();
} }
log.trace("[{}][{}] Saving new stats: {}", tenantId, ownerId, updatedEntries);
tsWsService.saveAndNotifyInternal(tenantId, usageState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK); tsWsService.saveAndNotifyInternal(tenantId, usageState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK);
if (!result.isEmpty()) { if (!result.isEmpty()) {
persistAndNotify(usageState, result); persistAndNotify(usageState, result);
@ -410,6 +414,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
myUsageStates.values().forEach(state -> { myUsageStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) { if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
}
saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values())); saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
if (state.getEntityType() == EntityType.TENANT && !state.getEntityId().equals(TenantId.SYS_TENANT_ID)) { if (state.getEntityType() == EntityType.TENANT && !state.getEntityId().equals(TenantId.SYS_TENANT_ID)) {
TenantId tenantId = state.getTenantId(); TenantId tenantId = state.getTenantId();
@ -417,6 +424,8 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
} }
} }
}); });
} catch (Throwable e) {
log.error("Failed to check start of next cycle", e);
} finally { } finally {
updateLock.unlock(); updateLock.unlock();
} }
@ -483,7 +492,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
} }
} }
state.setGaugeReportInterval(gaugeReportInterval); state.setGaugeReportInterval(gaugeReportInterval);
log.debug("[{}] Initialized state: {}", ownerId, storedState); log.debug("[{}][{}] Initialized state: {}", tenantId, ownerId, state);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId);
if (tpi.isMyPartition()) { if (tpi.isMyPartition()) {
addEntityState(tpi, state); addEntityState(tpi, state);

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

@ -92,12 +92,42 @@ public class DefaultDataUpdateService implements DataUpdateService {
log.info("Updating data from version 3.6.4 to 3.7.0 ..."); log.info("Updating data from version 3.6.4 to 3.7.0 ...");
updateCustomersWithTheSameTitle(); updateCustomersWithTheSameTitle();
updateMaxRuleNodeExecsPerMessage(); updateMaxRuleNodeExecsPerMessage();
updateGatewayRateLimits();
break; break;
default: default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
} }
} }
private void updateGatewayRateLimits() {
var tenantProfiles = new PageDataIterable<>(link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE);
tenantProfiles.forEach(tenantProfile -> {
var configurationOpt = tenantProfile.getProfileConfiguration();
configurationOpt.ifPresent(configuration -> {
boolean updated = false;
if (configuration.getTransportDeviceMsgRateLimit() != null && configuration.getTransportGatewayMsgRateLimit() == null) {
configuration.setTransportGatewayMsgRateLimit(configuration.getTransportDeviceMsgRateLimit());
updated = true;
}
if (configuration.getTransportDeviceTelemetryMsgRateLimit() != null && configuration.getTransportGatewayTelemetryMsgRateLimit() == null) {
configuration.setTransportGatewayTelemetryMsgRateLimit(configuration.getTransportDeviceTelemetryMsgRateLimit());
updated = true;
}
if (configuration.getTransportDeviceTelemetryDataPointsRateLimit() != null && configuration.getTransportGatewayTelemetryDataPointsRateLimit() == null) {
configuration.setTransportGatewayTelemetryDataPointsRateLimit(configuration.getTransportDeviceTelemetryDataPointsRateLimit());
updated = true;
}
if (updated) {
try {
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
} catch (Exception e) {
log.error("Failed to update tenant profile with id: {} due to: ", tenantProfile.getId(), e);
}
}
});
});
}
private void updateMaxRuleNodeExecsPerMessage() { private void updateMaxRuleNodeExecsPerMessage() {
var tenantProfiles = new PageDataIterable<>( var tenantProfiles = new PageDataIterable<>(
link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE); link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE);

28
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2024 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.mobile.secret;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface MobileAppSecretService {
String generateMobileAppSecret(SecurityUser securityUser);
JwtPair getJwtPair(String secret) throws ThingsboardException;
}

66
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java

@ -0,0 +1,66 @@
/**
* Copyright © 2016-2024 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.mobile.secret;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.dao.entity.AbstractCachedService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
@Service
@Slf4j
@RequiredArgsConstructor
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService {
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
@Override
public String generateMobileAppSecret(SecurityUser securityUser) {
log.trace("Executing generateSecret for user [{}]", securityUser.getId());
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength();
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
cache.put(secret, tokenFactory.createTokenPair(securityUser));
return secret;
}
@Override
public JwtPair getJwtPair(String secret) throws ThingsboardException {
TbCacheValueWrapper<JwtPair> jwtPair = cache.get(secret);
if (jwtPair != null) {
return jwtPair.get();
} else {
throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED);
}
}
@TransactionalEventListener(classes = MobileSecretEvictEvent.class)
@Override
public void handleEvictEvent(MobileSecretEvictEvent event) {
cache.evict(event.getSecret());
}
}

33
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2024 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.mobile.secret;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.security.model.JwtPair;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("MobileSecretCache")
public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache<String, JwtPair> {
public MobileSecretCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.MOBILE_SECRET_KEY_CACHE);
}
}

25
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2024 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.mobile.secret;
import lombok.Data;
@Data
public class MobileSecretEvictEvent {
private final String secret;
}

35
application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 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.mobile.secret;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.security.model.JwtPair;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("MobileSecretCache")
public class MobileSecretRedisCache extends RedisTbTransactionalCache<String, JwtPair> {
public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class));
}
}

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

@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
@ -59,8 +58,8 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg;
import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
@ -74,6 +73,7 @@ import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.TbRuleEngineProducerService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
@ -116,6 +116,9 @@ public class DefaultTbClusterService implements TbClusterService {
@Lazy @Lazy
private TbQueueProducerProvider producerProvider; private TbQueueProducerProvider producerProvider;
@Autowired
private TbRuleEngineProducerService ruleEngineProducerService;
@Autowired @Autowired
@Lazy @Lazy
private OtaPackageStateService otaPackageStateService; private OtaPackageStateService otaPackageStateService;
@ -203,13 +206,8 @@ public class DefaultTbClusterService implements TbClusterService {
HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId); HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId);
tbMsg = transformMsg(tbMsg, ruleEngineProfile); tbMsg = transformMsg(tbMsg, ruleEngineProfile);
} }
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId);
log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); ruleEngineProducerService.sendToRuleEngine(producerProvider.getRuleEngineMsgProducer(), tenantId, tbMsg, callback);
ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg)).build();
producerProvider.getRuleEngineMsgProducer().send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);
toRuleEngineMsgs.incrementAndGet(); toRuleEngineMsgs.incrementAndGet();
} }
@ -584,6 +582,7 @@ public class DefaultTbClusterService implements TbClusterService {
.setQueueName(queue.getName()) .setQueueName(queue.getName())
.setQueueTopic(queue.getTopic()) .setQueueTopic(queue.getTopic())
.setPartitions(queue.getPartitions()) .setPartitions(queue.getPartitions())
.setDuplicateMsgToAllPartitions(queue.isDuplicateMsgToAllPartitions())
.build()) .build())
.collect(Collectors.toList()); .collect(Collectors.toList());

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

@ -49,6 +49,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.DEVICE_PROFILE, profilePermissionChecker); put(Resource.DEVICE_PROFILE, profilePermissionChecker);
put(Resource.ASSET_PROFILE, profilePermissionChecker); put(Resource.ASSET_PROFILE, profilePermissionChecker);
put(Resource.TB_RESOURCE, customerResourcePermissionChecker); put(Resource.TB_RESOURCE, customerResourcePermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
} }
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {

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

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

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

@ -41,6 +41,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker);
put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker);
put(Resource.NOTIFICATION, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker);
} }
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {

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

@ -50,6 +50,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.QUEUE, queuePermissionChecker); put(Resource.QUEUE, queuePermissionChecker);
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, tenantEntityPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, new PermissionChecker.GenericPermissionChecker(Operation.READ));
} }
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

4
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -57,7 +57,6 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.utils.MiscUtils; import org.thingsboard.server.utils.MiscUtils;
import ua_parser.Client; import ua_parser.Client;
@ -75,6 +74,8 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING
@Slf4j @Slf4j
public class DefaultSystemSecurityService implements SystemSecurityService { public class DefaultSystemSecurityService implements SystemSecurityService {
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
@Autowired @Autowired
private AdminSettingsService adminSettingsService; private AdminSettingsService adminSettingsService;
@ -109,6 +110,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
securitySettings.setPasswordPolicy(new UserPasswordPolicy()); securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6); securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72); securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
} }
return securitySettings; return securitySettings;
} }

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

@ -95,7 +95,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private static final int DEFAULT_LIMIT = 100; private static final int DEFAULT_LIMIT = 100;
private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>(); private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
@Autowired @Lazy @Autowired
@Lazy
private WebSocketService wsService; private WebSocketService wsService;
@Autowired @Autowired
@ -728,7 +729,14 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
public void cancelAllSessionSubscriptions(String sessionId) { public void cancelAllSessionSubscriptions(String sessionId) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.remove(sessionId); Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.remove(sessionId);
if (sessionSubs != null) { if (sessionSubs != null) {
sessionSubs.values().forEach(this::cleanupAndCancel); sessionSubs.values().forEach(sub -> {
try {
cleanupAndCancel(sub);
} catch (Exception e) {
log.warn("[{}] Failed to remove subscription {} due to ", sub.getTenantId(), sub, e);
}
}
);
} }
} }

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

@ -45,6 +45,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
@ -62,6 +63,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@ -84,18 +87,21 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
private final PartitionService partitionService; private final PartitionService partitionService;
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final SubscriptionManagerService subscriptionManagerService; private final SubscriptionManagerService subscriptionManagerService;
private final WebSocketService webSocketService;
private ExecutorService tsCallBackExecutor; private ExecutorService tsCallBackExecutor;
private ScheduledExecutorService staleSessionCleanupExecutor;
public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider, public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider,
PartitionService partitionService, TbClusterService clusterService, PartitionService partitionService, TbClusterService clusterService,
@Lazy SubscriptionManagerService subscriptionManagerService) { @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) {
this.attrService = attrService; this.attrService = attrService;
this.tsService = tsService; this.tsService = tsService;
this.serviceInfoProvider = serviceInfoProvider; this.serviceInfoProvider = serviceInfoProvider;
this.partitionService = partitionService; this.partitionService = partitionService;
this.clusterService = clusterService; this.clusterService = clusterService;
this.subscriptionManagerService = subscriptionManagerService; this.subscriptionManagerService = subscriptionManagerService;
this.webSocketService = webSocketService;
} }
private String serviceId; private String serviceId;
@ -108,6 +114,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
subscriptionUpdateExecutor = ThingsBoardExecutors.newWorkStealingPool(20, getClass()); subscriptionUpdateExecutor = ThingsBoardExecutors.newWorkStealingPool(20, getClass());
tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-sub-callback")); tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-sub-callback"));
serviceId = serviceInfoProvider.getServiceId(); serviceId = serviceInfoProvider.getServiceId();
staleSessionCleanupExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("stale-session-cleanup"));
staleSessionCleanupExecutor.scheduleWithFixedDelay(this::cleanupStaleSessions, 60, 60, TimeUnit.SECONDS);
} }
@PreDestroy @PreDestroy
@ -118,6 +126,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (tsCallBackExecutor != null) { if (tsCallBackExecutor != null) {
tsCallBackExecutor.shutdownNow(); tsCallBackExecutor.shutdownNow();
} }
if (staleSessionCleanupExecutor != null) {
staleSessionCleanupExecutor.shutdownNow();
}
} }
@Override @Override
@ -157,9 +168,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
TenantId tenantId = subscription.getTenantId(); TenantId tenantId = subscription.getTenantId();
EntityId entityId = subscription.getEntityId(); EntityId entityId = subscription.getEntityId();
log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription); log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription);
Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); SubscriptionModificationResult result;
sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); subsLock.lock();
modifySubscription(tenantId, entityId, subscription, true); try {
Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>());
sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
result = modifySubscription(tenantId, entityId, subscription, true);
} finally {
subsLock.unlock();
}
if (result.hasEvent()) {
pushSubscriptionEvent(result);
}
} }
@Override @Override
@ -195,33 +215,49 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
@Override @Override
public void cancelSubscription(String sessionId, int subscriptionId) { public void cancelSubscription(String sessionId, int subscriptionId) {
log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); SubscriptionModificationResult result = null;
if (sessionSubscriptions != null) { subsLock.lock();
TbSubscription<?> subscription = sessionSubscriptions.remove(subscriptionId); try {
if (subscription != null) { Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.get(sessionId);
if (sessionSubscriptions.isEmpty()) { if (sessionSubscriptions != null) {
subscriptionsBySessionId.remove(sessionId); TbSubscription<?> subscription = sessionSubscriptions.remove(subscriptionId);
if (subscription != null) {
if (sessionSubscriptions.isEmpty()) {
subscriptionsBySessionId.remove(sessionId);
}
result = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false);
} else {
log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
} }
modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false);
} else { } else {
log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); log.debug("[{}] No session subscriptions found!", sessionId);
} }
} else { } finally {
log.debug("[{}] No session subscriptions found!", sessionId); subsLock.unlock();
}
if (result != null && result.hasEvent()) {
pushSubscriptionEvent(result);
} }
} }
@Override @Override
public void cancelAllSessionSubscriptions(String sessionId) { public void cancelAllSessionSubscriptions(String sessionId) {
log.debug("[{}] Going to remove session subscriptions.", sessionId); log.debug("[{}] Going to remove session subscriptions.", sessionId);
Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); List<SubscriptionModificationResult> results = new ArrayList<>();
if (sessionSubscriptions != null) { subsLock.lock();
for (TbSubscription<?> subscription : sessionSubscriptions.values()) { try {
modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId);
if (sessionSubscriptions != null) {
for (TbSubscription<?> subscription : sessionSubscriptions.values()) {
results.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false));
}
} else {
log.debug("[{}] No session subscriptions found!", sessionId);
} }
} else { } finally {
log.debug("[{}] No session subscriptions found!", sessionId); subsLock.unlock();
} }
results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent);
} }
@Override @Override
@ -384,10 +420,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
callback.onSuccess(); callback.onSuccess();
} }
private void modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription<?> subscription, boolean add) { private SubscriptionModificationResult modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription<?> subscription, boolean add) {
TbSubscription<?> missedUpdatesCandidate = null; TbSubscription<?> missedUpdatesCandidate = null;
TbEntitySubEvent event; TbEntitySubEvent event = null;
subsLock.lock();
try { try {
TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.computeIfAbsent(entityId.getId(), id -> new TbEntityLocalSubsInfo(tenantId, entityId)); TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.computeIfAbsent(entityId.getId(), id -> new TbEntityLocalSubsInfo(tenantId, entityId));
event = add ? entitySubs.add(subscription) : entitySubs.remove(subscription); event = add ? entitySubs.add(subscription) : entitySubs.remove(subscription);
@ -397,17 +432,23 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
} else if (add) { } else if (add) {
missedUpdatesCandidate = entitySubs.registerPendingSubscription(subscription, event); missedUpdatesCandidate = entitySubs.registerPendingSubscription(subscription, event);
} }
} finally { } catch (Exception e) {
subsLock.unlock(); log.warn("[{}][{}] Failed to {} subscription {} due to ", tenantId, entityId, add ? "add" : "remove", subscription, e);
} }
if (event != null) { return new SubscriptionModificationResult(tenantId, entityId, subscription, missedUpdatesCandidate, event);
log.trace("[{}][{}][{}] Event: {}", tenantId, entityId, subscription.getSubscriptionId(), event); }
pushSubEventToManagerService(tenantId, entityId, event);
private void pushSubscriptionEvent(SubscriptionModificationResult modificationResult) {
try {
TbEntitySubEvent event = modificationResult.getEvent();
log.trace("[{}][{}][{}] Event: {}", modificationResult.getTenantId(), modificationResult.getEntityId(), modificationResult.getSubscription().getSubscriptionId(), event);
pushSubEventToManagerService(modificationResult.getTenantId(), modificationResult.getEntityId(), event);
TbSubscription<?> missedUpdatesCandidate = modificationResult.getMissedUpdatesCandidate();
if (missedUpdatesCandidate != null) { if (missedUpdatesCandidate != null) {
checkMissedUpdates(missedUpdatesCandidate); checkMissedUpdates(missedUpdatesCandidate);
} }
} else { } catch (Exception e) {
log.trace("[{}][{}][{}] No changes detected.", tenantId, entityId, subscription.getSubscriptionId()); log.warn("[{}][{}] Failed to push subscription event {} due to ", modificationResult.getTenantId(), modificationResult.getEntityId(), modificationResult.getEvent(), e);
} }
} }
@ -518,4 +559,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
} }
} }
private void cleanupStaleSessions() {
subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale);
}
} }

39
application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2024 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 lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
/**
* The modification result of entity subscription
*/
@Builder
@Data
public class SubscriptionModificationResult {
private TenantId tenantId;
private EntityId entityId;
private TbSubscription<?> subscription;
private TbSubscription<?> missedUpdatesCandidate;
private TbEntitySubEvent event;
public boolean hasEvent() {
return event != null;
}
}

1
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -675,6 +675,7 @@ public class DefaultTransportApiService implements TransportApiService {
.setQueueName(queue.getName()) .setQueueName(queue.getName())
.setQueueTopic(queue.getTopic()) .setQueueTopic(queue.getTopic())
.setPartitions(queue.getPartitions()) .setPartitions(queue.getPartitions())
.setDuplicateMsgToAllPartitions(queue.isDuplicateMsgToAllPartitions())
.build()).collect(Collectors.toList())).build()); .build()).collect(Collectors.toList())).build());
} }

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

@ -83,6 +83,7 @@ import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpda
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -143,6 +144,7 @@ public class DefaultWebSocketService implements WebSocketService {
private final ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Map<Integer, Integer>> sessionCmdMap = new ConcurrentHashMap<>();
private ExecutorService executor; private ExecutorService executor;
private ScheduledExecutorService pingExecutor; private ScheduledExecutorService pingExecutor;
@ -197,12 +199,11 @@ public class DefaultWebSocketService implements WebSocketService {
wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
break; break;
case ERROR: case ERROR:
log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null)); log.debug("[{}] Unknown websocket session error: ", sessionId,
event.getError().orElse(new RuntimeException("No error specified")));
break; break;
case CLOSED: case CLOSED:
wsSessionsMap.remove(sessionId); cleanupSessionById(sessionId);
oldSubService.cancelAllSessionSubscriptions(sessionId);
entityDataSubService.cancelAllSessionSubscriptions(sessionId);
processSessionClose(sessionRef); processSessionClose(sessionRef);
break; break;
} }
@ -294,6 +295,14 @@ public class DefaultWebSocketService implements WebSocketService {
} }
} }
@Override
public void cleanupIfStale(String sessionId) {
if (!msgEndpoint.isOpen(sessionId)) {
log.info("[{}] Cleaning up stale session ", sessionId);
cleanupSessionById(sessionId);
}
}
private void processSessionClose(WebSocketSessionRef sessionRef) { private void processSessionClose(WebSocketSessionRef sessionRef) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration != null) { if (tenantProfileConfiguration != null) {
@ -440,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.queryTs(queryTs) .queryTs(queryTs)
@ -489,6 +498,13 @@ public class DefaultWebSocketService implements WebSocketService {
} }
} }
private int registerNewSessionSubId(String sessionId, WebSocketSessionRef sessionRef, int cmdId) {
var cmdMap = sessionCmdMap.computeIfAbsent(sessionId, id -> new ConcurrentHashMap<>());
var subId = sessionRef.getSessionSubIdSeq().incrementAndGet();
cmdMap.put(cmdId, subId);
return subId;
}
private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) { private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
if (!validateCmd(sessionRef, cmd, () -> { if (!validateCmd(sessionRef, cmd, () -> {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
@ -546,7 +562,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.queryTs(queryTs) .queryTs(queryTs)
@ -644,7 +660,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
@ -699,7 +715,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
@ -738,12 +754,25 @@ public class DefaultWebSocketService implements WebSocketService {
private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
oldSubService.cancelAllSessionSubscriptions(sessionId); log.warn("[{}][{}] Cleanup session due to empty entity id.", sessionId, cmd.getCmdId());
cleanupSessionById(sessionId);
} else { } else {
oldSubService.cancelSubscription(sessionId, cmd.getCmdId()); Integer subId = sessionCmdMap.getOrDefault(sessionId, Collections.emptyMap()).remove(cmd.getCmdId());
if (subId == null) {
log.trace("[{}][{}] Failed to lookup subscription id mapping", sessionId, cmd.getCmdId());
subId = cmd.getCmdId();
}
oldSubService.cancelSubscription(sessionId, subId);
} }
} }
private void cleanupSessionById(String sessionId) {
wsSessionsMap.remove(sessionId);
oldSubService.cancelAllSessionSubscriptions(sessionId);
sessionCmdMap.remove(sessionId);
entityDataSubService.cancelAllSessionSubscriptions(sessionId);
}
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
return validateCmd(sessionRef, cmd, () -> { return validateCmd(sessionRef, cmd, () -> {
if (cmd.getQuery() == null && !cmd.hasAnyCmd()) { if (cmd.getQuery() == null && !cmd.hasAnyCmd()) {

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

@ -29,4 +29,6 @@ public interface WebSocketMsgEndpoint {
void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException; void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException;
void close(WebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException; void close(WebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException;
boolean isOpen(String sessionId);
} }

3
application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java

@ -36,4 +36,7 @@ public interface WebSocketService {
void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg); void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg);
void close(String sessionId, CloseStatus status); void close(String sessionId, CloseStatus status);
void cleanupIfStale(String sessionId);
} }

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

@ -33,7 +33,7 @@ public class WebSocketSessionRef {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final String sessionId; private final String sessionId;
private SecurityUser securityCtx; private volatile SecurityUser securityCtx;
private final InetSocketAddress localAddress; private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress; private final InetSocketAddress remoteAddress;
private final WebSocketSessionType sessionType; private final WebSocketSessionType sessionType;

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

@ -587,6 +587,12 @@ cache:
alarmTypes: alarmTypes:
timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL
maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileAppSettings:
timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL
maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
mobileSecretKey:
timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL
maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above # Deliberately placed outside the 'specs' group above
notificationRules: notificationRules:
@ -1473,23 +1479,23 @@ queue:
# value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds)
topic-properties: topic-properties:
# Kafka properties for Rule Engine # Kafka properties for Rule Engine
rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for Core topics # Kafka properties for Core topics
core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for Transport Api topics # Kafka properties for Transport Api topics
transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}"
# Kafka properties for Notifications topics # Kafka properties for Notifications topics
notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for JS Executor topics # Kafka properties for JS Executor topics
js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}"
# Kafka properties for OTA updates topic # Kafka properties for OTA updates topic
ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}"
# Kafka properties for Version Control topic # Kafka properties for Version Control topic
version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for Housekeeper tasks topic # Kafka properties for Housekeeper tasks topic
housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}"
# Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time
housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
consumer-stats: consumer-stats:
# Prints lag between consumer group offset and last messages offset in Kafka topics # Prints lag between consumer group offset and last messages offset in Kafka topics
enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}"

6
application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java

@ -122,7 +122,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Assert.assertEquals(11, loadedAuditLogs.size()); Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = new ArrayList<>(); loadedAuditLogs = new ArrayList<>();
pageLink = new TimePageLink(5); pageLink = new TimePageLink(5);
@ -136,7 +136,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Assert.assertEquals(11, loadedAuditLogs.size()); Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = new ArrayList<>(); loadedAuditLogs = new ArrayList<>();
pageLink = new TimePageLink(5); pageLink = new TimePageLink(5);
@ -150,7 +150,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Assert.assertEquals(11, loadedAuditLogs.size()); Assert.assertEquals(11 + 1, loadedAuditLogs.size());
} }
@Test @Test

95
application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -25,9 +27,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategy;
@ -36,14 +42,19 @@ import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesDao; import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
@ -57,20 +68,25 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"queue.rule-engine.stats.max-error-message-length=100" "queue.rule-engine.stats.max-error-message-length=100",
"transport.http.enabled=true"
}) })
public class BaseQueueControllerTest extends AbstractControllerTest { public class BaseQueueControllerTest extends AbstractControllerTest {
@ -82,6 +98,12 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
private TimeseriesDao timeseriesDao; private TimeseriesDao timeseriesDao;
@Autowired @Autowired
private QueueStatsService queueStatsService; private QueueStatsService queueStatsService;
@SpyBean
private PartitionService partitionService;
@SpyBean
private TimeseriesService timeseriesService;
@SpyBean
private ActorSystemContext actorSystemContext;
@Test @Test
public void testQueueWithServiceTypeRE() throws Exception { public void testQueueWithServiceTypeRE() throws Exception {
@ -236,4 +258,75 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
assertThat(finalErrorMessage).isEqualTo(largeExceptionMessage.substring(0, 100) + "...[truncated 50 symbols]"); assertThat(finalErrorMessage).isEqualTo(largeExceptionMessage.substring(0, 100) + "...[truncated 50 symbols]");
} }
@Test
public void testMsgDuplicationToAllPartitions_fromTransport() throws Exception {
loginSysAdmin();
Queue queue = new Queue();
queue.setName("RealTime");
queue.setTopic("tb_rule_engine.real_time");
queue.setPollInterval(25);
int partitions = 12;
queue.setPartitions(partitions);
queue.setTenantId(TenantId.SYS_TENANT_ID);
queue.setConsumerPerPartition(true);
queue.setPackProcessingTimeout(2000);
SubmitStrategy submitStrategy = new SubmitStrategy();
submitStrategy.setType(SubmitStrategyType.BURST);
queue.setSubmitStrategy(submitStrategy);
ProcessingStrategy processingStrategy = new ProcessingStrategy();
processingStrategy.setType(ProcessingStrategyType.RETRY_ALL);
processingStrategy.setRetries(0);
processingStrategy.setPauseBetweenRetries(3);
processingStrategy.setMaxPauseBetweenRetries(5);
queue.setProcessingStrategy(processingStrategy);
queue.setAdditionalInfo(JacksonUtil.newObjectNode()
.put("duplicateMsgToAllPartitions", true));
queue = saveQueue(queue);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, "RealTime", tenantId, tenantId)).hasSize(partitions);
});
loginTenantAdmin();
DeviceProfile deviceProfile = createDeviceProfile("realtime");
deviceProfile.setDefaultQueueName(queue.getName());
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
Device device = createDevice("test", deviceProfile.getName(), "test-token");
JsonNode payload = JacksonUtil.newObjectNode()
.put("test", "test");
doPost("/api/v1/test-token/telemetry", payload).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verify(timeseriesService, times(partitions)).save(eq(tenantId), eq(device.getId()),
argThat(ts -> ts.size() == 1 && ts.get(0).getKey().equals("test")), anyLong());
ArgumentCaptor<TbActorMsg> msgCaptor = ArgumentCaptor.forClass(TbActorMsg.class);
verify(actorSystemContext, atLeastOnce()).tell(msgCaptor.capture());
List<TbMsg> tbMsgs = msgCaptor.getAllValues().stream()
.map(actorMsg -> actorMsg instanceof QueueToRuleEngineMsg queueToRuleEngineMsg ? queueToRuleEngineMsg.getMsg() : null)
.filter(tbMsg -> tbMsg != null && tbMsg.getCorrelationId() != null && tbMsg.getInternalType() == TbMsgType.POST_TELEMETRY_REQUEST)
.toList();
assertThat(tbMsgs).hasSize(partitions);
UUID correlationId = tbMsgs.get(0).getCorrelationId();
assertThat(tbMsgs).extracting(TbMsg::getCorrelationId).containsOnly(correlationId);
assertThat(tbMsgs).extracting(TbMsg::getId).doesNotHaveDuplicates();
assertThat(tbMsgs).extracting(TbMsg::getPartition).containsExactlyInAnyOrder(IntStream.range(0, partitions).boxed().toArray(Integer[]::new));
});
loginSysAdmin();
((ObjectNode) queue.getAdditionalInfo()).put("duplicateMsgToAllPartitions", false);
queue = saveQueue(queue);
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, "RealTime", tenantId, tenantId)).hasSize(1);
});
doDelete("/api/queues/" + queue.getUuidId()).andExpect(status().isOk());
}
private Queue saveQueue(Queue queue) {
return doPost("/api/queues?serviceType=TB_RULE_ENGINE", queue, Queue.class);
}
} }

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

@ -53,8 +53,12 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -423,6 +427,65 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
} }
@Test
public void testFindEntityDataByQueryWithNegateParam() throws Exception {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Device device = new Device();
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
Device mainDevice = new Device();
mainDevice.setName("Main device");
mainDevice = doPost("/api/device", mainDevice, Device.class);
for (int i = 0; i < 10; i++) {
EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
}
for (int i = 10; i < 97; i++) {
EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "NOT_CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
}
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(mainDevice.getId());
filter.setDirection(EntitySearchDirection.FROM);
filter.setNegate(true);
filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false)));
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(87, data.getTotalElements());
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(10, data.getTotalElements());
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(87, data.getTotalElements());
}
private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) {
return new EntityRelation(mainDevice.getId(), device.getId(), relationType);
}
@Test @Test
public void testFindEntityDataByQueryWithAttributes() throws Exception { public void testFindEntityDataByQueryWithAttributes() throws Exception {
List<Device> devices = new ArrayList<>(); List<Device> devices = new ArrayList<>();

253
application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java

@ -0,0 +1,253 @@
/**
* Copyright © 2016-2024 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 com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.mobile.AndroidConfig;
import org.thingsboard.server.common.data.mobile.IosConfig;
import org.thingsboard.server.common.data.mobile.MobileAppSettings;
import org.thingsboard.server.common.data.mobile.QRCodeConfig;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class MobileApplicationControllerTest extends AbstractControllerTest {
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}")
private int mobileSecretKeyTtl;
private static final String ANDROID_PACKAGE_NAME = "testAppPackage";
private static final String ANDROID_APP_SHA256 = "DF:28:32:66:8B:A7:D3:EC:7D:73:CF:CC";
private static final String APPLE_APP_ID = "testId";
private static final String TEST_LABEL = "Test label";
@Before
public void setUp() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
QRCodeConfig qrCodeConfig = new QRCodeConfig();
qrCodeConfig.setQrCodeLabel(TEST_LABEL);
mobileAppSettings.setUseDefaultApp(true);
AndroidConfig androidConfig = AndroidConfig.builder()
.appPackage(ANDROID_PACKAGE_NAME)
.sha256CertFingerprints(ANDROID_APP_SHA256)
.enabled(true)
.build();
IosConfig iosConfig = IosConfig.builder()
.appId(APPLE_APP_ID)
.enabled(true)
.build();
mobileAppSettings.setAndroidConfig(androidConfig);
mobileAppSettings.setIosConfig(iosConfig);
mobileAppSettings.setQrCodeConfig(qrCodeConfig);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testSaveMobileAppSettings() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL);
assertThat(mobileAppSettings.isUseDefaultApp()).isTrue();
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse();
}
@Test
public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
mobileAppSettings.setAndroidConfig(null);
mobileAppSettings.setIosConfig(null);
mobileAppSettings.setQrCodeConfig(null);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!")));
mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build());
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!")));
mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build());
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Qr code configuration is required!")));
mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build());
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
AndroidConfig androidConfig = AndroidConfig.builder()
.enabled(true)
.appPackage(null)
.sha256CertFingerprints(null)
.build();
mobileAppSettings.setAndroidConfig(androidConfig);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
androidConfig.setAppPackage("test_app_package");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!")));
androidConfig.setSha256CertFingerprints("test_sha_256");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
IosConfig iosConfig = IosConfig.builder()
.enabled(true)
.appId(null)
.build();
mobileAppSettings.setIosConfig(iosConfig);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Application id is required for custom ios application!")));
iosConfig.setAppId("test_app_id");
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(true);
mobileAppSettings.setIosConfig(null);
mobileAppSettings.setAndroidConfig(null);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
}
@Test
public void testGetApplicationAssociations() throws Exception {
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings)
.andExpect(status().isOk());
JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class);
assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME);
assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256);
JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class);
assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID);
}
@Test
public void testGetMobileDeepLink() throws Exception {
loginSysAdmin();
String deepLink = doGet("/api/mobile/deepLink", String.class);
Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\"");
Matcher parsedDeepLink = expectedPattern.matcher(deepLink);
assertThat(parsedDeepLink.matches()).isTrue();
String appHost = parsedDeepLink.group(1);
String secret = parsedDeepLink.group(2);
String ttl = parsedDeepLink.group(3);
assertThat(appHost).isEqualTo("demo.thingsboard.io");
assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl));
JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class);
assertThat(jwtPair).isNotNull();
loginTenantAdmin();
String tenantDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink);
assertThat(tenantParsedDeepLink.matches()).isTrue();
String tenantSecret = tenantParsedDeepLink.group(2);
JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class);
assertThat(tenantJwtPair).isNotNull();
loginCustomerUser();
String customerDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink);
assertThat(customerParsedDeepLink.matches()).isTrue();
String customerSecret = customerParsedDeepLink.group(2);
JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class);
assertThat(customerJwtPair).isNotNull();
// update mobile setting to use custom one
loginSysAdmin();
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class);
mobileAppSettings.setUseDefaultApp(false);
doPost("/api/mobile/app/settings", mobileAppSettings);
String customAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\"");
Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink);
assertThat(customAppParsedDeepLink.matches()).isTrue();
assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginTenantAdmin();
String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink);
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue();
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
loginCustomerUser();
String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class);
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink);
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue();
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost");
}
}

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

@ -298,15 +298,13 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
logInWithPreVerificationToken(username, password); logInWithPreVerificationToken(username, password);
await("async audit log saving").during(1, TimeUnit.SECONDS); await("async audit log saving").during(1, TimeUnit.SECONDS);
assertThat(getLogInAuditLogs()).isEmpty();
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
.get("lastLoginTs")).isNull();
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
// there is the first login audit log after user activation
await("async audit log saving").atMost(1, TimeUnit.SECONDS) await("async audit log saving").atMost(1, TimeUnit.SECONDS)
.until(() -> getLogInAuditLogs().size() == 1); .until(() -> getLogInAuditLogs().size() == 2);
assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> {
assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE);
assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect");
@ -316,7 +314,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig))
.andExpect(status().isOk()); .andExpect(status().isOk());
await("async audit log saving").atMost(1, TimeUnit.SECONDS) await("async audit log saving").atMost(1, TimeUnit.SECONDS)
.until(() -> getLogInAuditLogs().size() == 2); .until(() -> getLogInAuditLogs().size() == 3);
assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> {
assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS);
assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username);

1
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -101,7 +101,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"transport.http.enabled=true",
"queue.core.housekeeper.task-reprocessing-delay-ms=2000", "queue.core.housekeeper.task-reprocessing-delay-ms=2000",
"queue.core.housekeeper.poll-interval-ms=1000", "queue.core.housekeeper.poll-interval-ms=1000",
"queue.core.housekeeper.max-reprocessing-attempts=5" "queue.core.housekeeper.max-reprocessing-attempts=5"

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

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

2
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -331,7 +331,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testNotificationUpdatesForSeveralUsers() throws Exception { public void testNotificationUpdatesForSeveralUsers() throws Exception {
int usersCount = 150; int usersCount = 50;
Map<User, NotificationApiWsClient> sessions = new HashMap<>(); Map<User, NotificationApiWsClient> sessions = new HashMap<>();
List<NotificationTargetId> targets = new ArrayList<>(); List<NotificationTargetId> targets = new ArrayList<>();

3
application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java

@ -33,6 +33,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.TbRuleEngineProducerService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
@ -77,6 +78,8 @@ public class DefaultTbClusterServiceTest {
protected PartitionService partitionService; protected PartitionService partitionService;
@MockBean @MockBean
protected TbQueueProducerProvider producerProvider; protected TbQueueProducerProvider producerProvider;
@MockBean
protected TbRuleEngineProducerService ruleEngineProducerService;
@SpyBean @SpyBean
protected TopicService topicService; protected TopicService topicService;

5
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -178,6 +178,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
private String[] resources; private String[] resources;
protected String deviceId; protected String deviceId;
protected boolean isWriteAttribute = false; protected boolean isWriteAttribute = false;
protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false;
@Before @Before
public void startInit() throws Exception { public void startInit() throws Exception {
@ -318,8 +319,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
try (ServerSocket socket = new ServerSocket(0)) { try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort(); int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, securityBs, clientPort, isRpc, lwM2MTestClient.init(security, securityBs, clientPort, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute,
, clientDtlsCidLength, queueMode); clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR);
} }
} }

33
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -47,10 +47,19 @@ import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder;
import org.eclipse.leshan.core.node.codec.NodeDecoder;
import org.eclipse.leshan.core.node.codec.NodeEncoder;
import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborDecoder;
import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborEncoder;
import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLDecoder;
import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLEncoder;
import org.eclipse.leshan.core.request.BootstrapRequest; import org.eclipse.leshan.core.request.BootstrapRequest;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.DeregisterRequest; import org.eclipse.leshan.core.request.DeregisterRequest;
import org.eclipse.leshan.core.request.RegisterRequest; import org.eclipse.leshan.core.request.RegisterRequest;
import org.eclipse.leshan.core.request.UpdateRequest; import org.eclipse.leshan.core.request.UpdateRequest;
import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder;
import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
import org.junit.Assert; import org.junit.Assert;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
@ -78,6 +87,7 @@ import static org.eclipse.leshan.core.LwM2mId.LOCATION;
import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId;
@ -130,7 +140,8 @@ public class LwM2MTestClient {
public void init(Security security, Security securityBs, int port, boolean isRpc, public void init(Security security, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode) throws InvalidDDFFileException, IOException { LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient); Assert.assertNull("client already initialized", leshanClient);
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
this.clientContext = clientContext; this.clientContext = clientContext;
@ -274,12 +285,28 @@ public class LwM2MTestClient {
builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()])); builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()]));
builder.setDataSenders(new ManualDataSender()); builder.setDataSenders(new ManualDataSender());
builder.setRegistrationEngineFactory(engineFactory); builder.setRegistrationEngineFactory(engineFactory);
boolean supportOldFormat = true; Map<ContentFormat, NodeDecoder> decoders = new HashMap<>();
if (supportOldFormat) { Map<ContentFormat, NodeEncoder> encoders = new HashMap<>();
if (supportFormatOnly_SenMLJSON_SenMLCBOR) {
// decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder());
decoders.put(ContentFormat.CBOR, new LwM2mNodeCborDecoder());
decoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLDecoder(new SenMLJsonJacksonEncoderDecoder(), true));
decoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLDecoder(new SenMLCborUpokecenterEncoderDecoder(), false));
builder.setDecoder(new DefaultLwM2mDecoder(decoders));
// encoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueEncoder());
encoders.put(ContentFormat.CBOR, new LwM2mNodeCborEncoder());
encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLEncoder(new SenMLJsonJacksonEncoderDecoder()));
encoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLEncoder(new SenMLCborUpokecenterEncoderDecoder()));
builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), false));
builder.setEncoder(new DefaultLwM2mEncoder(encoders, getDefaultPathEncoder(), new LwM2mValueConverterImpl()));
} else {
boolean supportOldFormat = true;
builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat)); builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat));
builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat)); builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat));
} }
builder.setRegistrationEngineFactory(engineFactory); builder.setRegistrationEngineFactory(engineFactory);
builder.setSharedExecutor(executor); builder.setSharedExecutor(executor);

3
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -86,6 +86,9 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){ if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){
isWriteAttribute = true; isWriteAttribute = true;
} }
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")){
supportFormatOnly_SenMLJSON_SenMLCBOR = true;
}
initRpc(); initRpc();
} }

98
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteCborTest.java

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2024 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.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
public class RpcLwm2mIntegrationWriteCborTest extends AbstractRpcLwM2MIntegrationTest {
/**
* Client with ContentFormat ={}
*/
/**
* Resource:
* <Type>Opaque</Type>
* Input type String (HexDec)
* WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890CAB"}}
* return Hex.decodeHex(((String)value).toCharArray()) - ok [1, 35, 69, 103, -119, 12, -85, -1]
* {"result":"CHANGED"} -> Actual Value Equals expectedValue0
*/
@Test
public void testWriteReplaceValueMultipleResource_InputFormatData_Ok_Result_CHANGED_Value_Equals_expectedValue0() throws Exception {
String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
int resourceInstanceId0 = 0;
String expectedValue0 = "01234567890CABFF";
String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}";
String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
String expectedPath0 = expectedPath + "/" + resourceInstanceId0;
actualResult = sendRPCReadById(expectedPath0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String actualValues = rpcActualResult.get("value").asText();
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]";
assertTrue(actualValues.contains(expected));
}
/**
* Resource:
* <Type>Opaque</Type>
* Input type String (not HexDec)
* return Hex.decodeHex(((String)value).toCharArray()) - error
* return Base64.getDecoder().decode(((String) value).getBytes()) - ok;
* WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890"}}
* {"result":"CHANGED"} -> Actual Value Not Equals expectedValue0
*/
@Test
public void testWriteReplaceValueMultipleResource_Error_InputFormatData_Result_CHANGED_Value_Not_Equals_expectedValue0() throws Exception {
String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
int resourceInstanceId0 = 0;
String expectedValue0 = "01234567890";
String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}";
String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
String expectedPath0 = expectedPath + "/" + resourceInstanceId0;
actualResult = sendRPCReadById(expectedPath0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String actualValues = rpcActualResult.get("value").asText();
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]";
assertFalse(actualValues.contains(expected));
}
private String sendRPCWriteObjectById(String method, String path, Object value) throws Exception {
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
private String sendRPCReadById(String id) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + id + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}
}

27
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java

@ -23,6 +23,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
@ -107,6 +108,32 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
assertTrue(actualValues.contains(expected)); assertTrue(actualValues.contains(expected));
} }
/**
* Resource:
* <Type>Opaque</Type>
* Input type String (not HexDec)
* return Hex.decodeHex(((String)value).toCharArray()) - error
* return Base64.getDecoder().decode(((String) value).getBytes()) - ok;
* WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890"}}
* {"result":"CHANGED"} -> Actual Value Not Equals expectedValue0
*/
@Test
public void testWriteReplaceValueMultipleResource_Error_InputFormatData_Result_CHANGED_Value_Not_Equals_expectedValue0() throws Exception {
String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
int resourceInstanceId0 = 0;
String expectedValue0 = "01234567890";
String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}";
String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
String expectedPath0 = expectedPath + "/" + resourceInstanceId0;
actualResult = sendRPCReadById(expectedPath0);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String actualValues = rpcActualResult.get("value").asText();
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]";
assertFalse(actualValues.contains(expected));
}
/** /**
* bad: singleResource, operation="R" - only read * bad: singleResource, operation="R" - only read
* WriteReplace {"id":"/3/0/9","value":90} * WriteReplace {"id":"/3/0/9","value":90}

172
application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java

@ -0,0 +1,172 @@
/**
* Copyright © 2016-2024 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.transport.mqtt;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY;
@DaoSqlTest
@TestPropertySource(properties = {
"service.integrations.supported=ALL",
"transport.mqtt.enabled=true",
})
public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
private static final String TOPIC = "v1/gateway/telemetry";
private static final String DEVICE_A = "DeviceA";
private static final String DEVICE_B = "DeviceB";
private DeviceId gatewayId;
private String gatewayAccessToken;
@SpyBean
private NotificationRuleProcessor notificationRuleProcessor;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
Assert.assertNotNull(tenantProfile);
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration();
profileConfiguration.setTransportGatewayMsgRateLimit(null);
profileConfiguration.setTransportGatewayTelemetryMsgRateLimit(null);
profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit(null);
doPost("/api/tenantProfile", tenantProfile);
loginTenantAdmin();
createGateway();
Mockito.reset(notificationRuleProcessor);
}
@Test
public void transportGatewayMsgRateLimitTest() throws Exception {
// Device A 2 msgs success ('create device', 'to device actor'), Device B 'create device success' , 'to device actor' - limited
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("3:600"));
}
@Test
public void transportGatewayTelemetryMsgRateLimitTest() throws Exception {
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("1:600"));
}
@Test
public void transportGatewayTelemetryDataPointsRateLimitTest() throws Exception {
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("1:600"));
}
private void transportGatewayRateLimitTest(Consumer<DefaultTenantProfileConfiguration> profileConfiguration) throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
Assert.assertNotNull(tenantProfile);
profileConfiguration.accept((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration());
doPost("/api/tenantProfile", tenantProfile);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_A));
loginTenantAdmin();
Device deviceA = getDeviceByName(DEVICE_A);
var deviceATrigger = createRateLimitsTrigger(deviceA);
Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(deviceATrigger));
try {
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_B));
} catch (Exception t) {
}
Device deviceB = getDeviceByName(DEVICE_B);
var deviceBTrigger = createRateLimitsTrigger(deviceB);
Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(deviceBTrigger);
if (client.isConnected()) {
client.disconnect();
}
}
private void createGateway() throws Exception {
Device device = new Device();
device.setName("gateway");
ObjectNode additionalInfo = JacksonUtil.newObjectNode();
additionalInfo.put("gateway", true);
device.setAdditionalInfo(additionalInfo);
device = doPost("/api/device", device, Device.class);
assertNotNull(device);
gatewayId = device.getId();
assertNotNull(gatewayId);
DeviceCredentials deviceCredentials = doGet("/api/device/" + gatewayId + "/credentials", DeviceCredentials.class);
assertNotNull(deviceCredentials);
assertEquals(gatewayId, deviceCredentials.getDeviceId());
gatewayAccessToken = deviceCredentials.getCredentialsId();
assertNotNull(gatewayAccessToken);
}
private Device getDeviceByName(String deviceName) throws Exception {
Device device = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
assertNotNull(device);
return device;
}
private byte[] getGatewayPayload(String deviceName) {
return String.format("{\"%s\": [{\"values\": {\"temperature\": 42}}]}", deviceName).getBytes();
}
private RateLimitsTrigger createRateLimitsTrigger(Device device) {
return RateLimitsTrigger.builder()
.tenantId(tenantId)
.api(TRANSPORT_MESSAGES_PER_GATEWAY)
.limitLevel(device.getId())
.limitLevelEntityName(device.getName())
.build();
}
}

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

@ -48,4 +48,6 @@ public class CacheConstants {
public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo"; public static final String RESOURCE_INFO_CACHE = "resourceInfo";
public static final String ALARM_TYPES_CACHE = "alarmTypes"; public static final String ALARM_TYPES_CACHE = "alarmTypes";
public static final String MOBILE_APP_SETTINGS_CACHE = "mobileAppSettings";
public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey";
} }

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

@ -31,4 +31,5 @@ public class SystemParams {
JsonNode userSettings; JsonNode userSettings;
long maxDatapointsLimit; long maxDatapointsLimit;
long maxResourceSize; long maxResourceSize;
boolean mobileQrEnabled;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java

@ -49,7 +49,7 @@ public class AuditLog extends BaseData<AuditLogId> {
private ActionType actionType; private ActionType actionType;
@Schema(description = "JsonNode represented action data", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JsonNode represented action data", accessMode = Schema.AccessMode.READ_ONLY)
private JsonNode actionData; private JsonNode actionData;
@Schema(description = "String represented Action status", example = "SUCCESS", allowableValues = "SUCCESS,FAILURE", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "String represented Action status", example = "SUCCESS", allowableValues = {"SUCCESS", "FAILURE"}, accessMode = Schema.AccessMode.READ_ONLY)
private ActionStatus actionStatus; private ActionStatus actionStatus;
@Schema(description = "Failure action details info. An empty string in case of action status type 'SUCCESS', otherwise includes stack trace of the caused exception.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "Failure action details info. An empty string in case of action status type 'SUCCESS', otherwise includes stack trace of the caused exception.", accessMode = Schema.AccessMode.READ_ONLY)
private String actionFailureDetails; private String actionFailureDetails;

2
common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java

@ -25,7 +25,7 @@ public abstract class DebugEventFilter implements EventFilter {
@Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") @Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152")
protected String server; protected String server;
@Schema(description = "Boolean value to filter the errors", allowableValues = "false, true") @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"})
protected boolean isError; protected boolean isError;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr; protected String errorStr;

2
common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java

@ -27,7 +27,7 @@ public class LifeCycleEventFilter implements EventFilter {
protected String server; protected String server;
@Schema(description = "String value representing the lifecycle event type", example = "STARTED") @Schema(description = "String value representing the lifecycle event type", example = "STARTED")
protected String event; protected String event;
@Schema(description = "String value representing status of the lifecycle event", allowableValues = "Success, Failure") @Schema(description = "String value representing status of the lifecycle event", allowableValues = {"Success", "Failure"})
protected String status; protected String status;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr; protected String errorStr;

2
common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java

@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.StringUtils;
@Schema @Schema
public class RuleNodeDebugEventFilter extends DebugEventFilter { public class RuleNodeDebugEventFilter extends DebugEventFilter {
@Schema(description = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = "IN, OUT") @Schema(description = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = {"IN", "OUT"})
protected String msgDirectionType; protected String msgDirectionType;
@Schema(description = "String value representing the entity id in the event body (originator of the message)", example = "de9d54a0-2b7a-11ec-a3cc-23386423d98f") @Schema(description = "String value representing the entity id in the event body (originator of the message)", example = "de9d54a0-2b7a-11ec-a3cc-23386423d98f")
protected String entityId; protected String entityId;

37
common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2024 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 java.util.UUID;
@Schema
public class MobileAppSettingsId extends UUIDBased {
private static final long serialVersionUID = 1L;
@JsonCreator
public MobileAppSettingsId(@JsonProperty("id") UUID id) {
super(id);
}
public static MobileAppSettingsId fromString(String mobileAppSettingsId) {
return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId));
}
}

1
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -40,6 +40,7 @@ public enum LimitedApi {
TWO_FA_VERIFICATION_CODE_CHECK(false, true), TWO_FA_VERIFICATION_CODE_CHECK(false, true),
TRANSPORT_MESSAGES_PER_TENANT("transport messages", true), TRANSPORT_MESSAGES_PER_TENANT("transport messages", true),
TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false),
TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false),
EMAILS("emails sending", true); EMAILS("emails sending", true);
private Function<DefaultTenantProfileConfiguration, String> configExtractor; private Function<DefaultTenantProfileConfiguration, String> configExtractor;

38
common/data/src/main/java/org/thingsboard/server/common/data/mobile/AndroidConfig.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2024 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.mobile;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class AndroidConfig {
private boolean enabled;
@NoXss
private String appPackage;
@NoXss
private String sha256CertFingerprints;
}

23
common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgePosition.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2024 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.mobile;
public enum BadgePosition {
RIGHT,
LEFT;
}

24
common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2024 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.mobile;
public enum BadgeStyle {
ORIGINAL,
WHITE;
}

36
common/data/src/main/java/org/thingsboard/server/common/data/mobile/IosConfig.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2024 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.mobile;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class IosConfig {
private boolean enabled;
@NoXss
private String appId;
}

45
common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2024 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.mobile;
import jakarta.validation.Valid;
import lombok.Data;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.MobileAppSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements HasTenantId {
private static final long serialVersionUID = 2628323657987010348L;
private TenantId tenantId;
private boolean useDefaultApp;
@Valid
private AndroidConfig androidConfig;
@Valid
private IosConfig iosConfig;
@Valid
private QRCodeConfig qrCodeConfig;
public MobileAppSettings() {
}
public MobileAppSettings(MobileAppSettingsId id) {
super(id);
}
}

40
common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2024 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.mobile;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class QRCodeConfig {
private boolean showOnHomePage;
private boolean badgeEnabled;
private boolean qrCodeLabelEnabled;
private BadgePosition badgePosition;
private BadgeStyle badgeStyle;
@NoXss
private String qrCodeLabel;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java

@ -39,7 +39,7 @@ public class ComponentDescriptor extends BaseData<ComponentDescriptorId> {
@Getter @Setter private ComponentType type; @Getter @Setter private ComponentType type;
@Schema(description = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT") @Schema(description = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT")
@Getter @Setter private ComponentScope scope; @Getter @Setter private ComponentScope scope;
@Schema(description = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "USER_PREFERENCE, ENABLED, SINGLETON", example = "ENABLED") @Schema(description = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = {"USER_PREFERENCE", "ENABLED", "SINGLETON"}, example = "ENABLED")
@Getter @Setter private ComponentClusteringMode clusteringMode; @Getter @Setter private ComponentClusteringMode clusteringMode;
@Length(fieldName = "name") @Length(fieldName = "name")
@Schema(description = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = Schema.AccessMode.READ_ONLY, example = "Custom Rule Node") @Schema(description = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = Schema.AccessMode.READ_ONLY, example = "Custom Rule Node")

1
common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java

@ -40,5 +40,6 @@ public class RelationsQueryFilter implements EntityFilter {
private List<RelationEntityTypeFilter> filters; private List<RelationEntityTypeFilter> filters;
private int maxLevel; private int maxLevel;
private boolean fetchLastLevelOnly; private boolean fetchLastLevelOnly;
private boolean negate;
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java

@ -73,4 +73,11 @@ public class Queue extends BaseDataWithAdditionalInfo<QueueId> implements HasNam
.filter(JsonNode::isTextual).map(JsonNode::asText).orElse(null); .filter(JsonNode::isTextual).map(JsonNode::asText).orElse(null);
} }
@JsonIgnore
public boolean isDuplicateMsgToAllPartitions() {
return Optional.ofNullable(getAdditionalInfo())
.map(info -> info.get("duplicateMsgToAllPartitions"))
.filter(JsonNode::isBoolean).map(JsonNode::asBoolean).orElse(false);
}
} }

17
common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java

@ -16,7 +16,6 @@
package org.thingsboard.server.common.data.relation; package org.thingsboard.server.common.data.relation;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -26,13 +25,27 @@ import java.util.List;
* Created by ashvayka on 02.05.17. * Created by ashvayka on 02.05.17.
*/ */
@Data @Data
@AllArgsConstructor
@Schema @Schema
public class RelationEntityTypeFilter { public class RelationEntityTypeFilter {
public RelationEntityTypeFilter() {}
public RelationEntityTypeFilter(String relationType, List<EntityType> entityTypes) {
this(relationType, entityTypes, false);
}
public RelationEntityTypeFilter(String relationType, List<EntityType> entityTypes, boolean negate) {
this.relationType = relationType;
this.entityTypes = entityTypes;
this.negate = negate;
}
@Schema(description = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains") @Schema(description = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains")
private String relationType; private String relationType;
@Schema(description = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').") @Schema(description = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').")
private List<EntityType> entityTypes; private List<EntityType> entityTypes;
@Schema(description = "Negate relation type between root entity and other entity.")
private boolean negate;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java

@ -70,7 +70,7 @@ public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements
this.deviceId = deviceId; this.deviceId = deviceId;
} }
@Schema(description = "Type of the credentials", allowableValues ="ACCESS_TOKEN, X509_CERTIFICATE, MQTT_BASIC, LWM2M_CREDENTIALS") @Schema(description = "Type of the credentials", allowableValues = {"ACCESS_TOKEN", "X509_CERTIFICATE", "MQTT_BASIC", "LWM2M_CREDENTIALS"})
@Override @Override
public DeviceCredentialsType getCredentialsType() { public DeviceCredentialsType getCredentialsType() {
return credentialsType; return credentialsType;

4
common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java

@ -20,10 +20,12 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import java.io.Serializable;
@Schema(description = "JWT Pair") @Schema(description = "JWT Pair")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
public class JwtPair { public class JwtPair implements Serializable {
@Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") @Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
private String token; private String token;

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java

@ -32,4 +32,6 @@ public class SecuritySettings implements Serializable {
private Integer maxFailedLoginAttempts; private Integer maxFailedLoginAttempts;
@Schema(description = "Email to use for notifications about locked users." ) @Schema(description = "Email to use for notifications about locked users." )
private String userLockoutNotificationEmail; private String userLockoutNotificationEmail;
@Schema(description = "Mobile secret key length" )
private Integer mobileSecretKeyLength;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java

@ -89,7 +89,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration {
@Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String addressRange; private String addressRange;
@Schema(allowableValues = "0-10,13-14", @Schema(allowableValues = {"0-10" ,"13-14"},
description = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" + description = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" +
"1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" + "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" +
"2 - Octet Unspecified (8-bit binary)\n" + "2 - Octet Unspecified (8-bit binary)\n" +

3
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -47,6 +47,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String transportDeviceMsgRateLimit; private String transportDeviceMsgRateLimit;
private String transportDeviceTelemetryMsgRateLimit; private String transportDeviceTelemetryMsgRateLimit;
private String transportDeviceTelemetryDataPointsRateLimit; private String transportDeviceTelemetryDataPointsRateLimit;
private String transportGatewayMsgRateLimit;
private String transportGatewayTelemetryMsgRateLimit;
private String transportGatewayTelemetryDataPointsRateLimit;
private String tenantEntityExportRateLimit; private String tenantEntityExportRateLimit;
private String tenantEntityImportRateLimit; private String tenantEntityImportRateLimit;

4
common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java

@ -23,4 +23,8 @@ import lombok.Data;
public class TbPair<S, T> { public class TbPair<S, T> {
private S first; private S first;
private T second; private T second;
public static <S, T> TbPair<S, T> of(S first, T second) {
return new TbPair<>(first, second);
}
} }

61
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -42,6 +44,8 @@ import java.util.UUID;
*/ */
@Data @Data
@Slf4j @Slf4j
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(toBuilder = true)
public final class TbMsg implements Serializable { public final class TbMsg implements Serializable {
public static final String EMPTY_JSON_OBJECT = "{}"; public static final String EMPTY_JSON_OBJECT = "{}";
@ -60,6 +64,10 @@ public final class TbMsg implements Serializable {
private final String data; private final String data;
private final RuleChainId ruleChainId; private final RuleChainId ruleChainId;
private final RuleNodeId ruleNodeId; private final RuleNodeId ruleNodeId;
private final UUID correlationId;
private final Integer partition;
@Getter(value = AccessLevel.NONE) @Getter(value = AccessLevel.NONE)
@JsonIgnore @JsonIgnore
//This field is not serialized because we use queues and there is no need to do it //This field is not serialized because we use queues and there is no need to do it
@ -257,7 +265,7 @@ public final class TbMsg implements Serializable {
@Deprecated(since = "3.6.0") @Deprecated(since = "3.6.0")
public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.callback);
} }
public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
@ -271,54 +279,54 @@ public final class TbMsg implements Serializable {
} }
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, type.name(), originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.callback);
} }
public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) { public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgData(TbMsg tbMsg, String data) { public static TbMsg transformMsgData(TbMsg tbMsg, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) { public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) { public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) { public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) { public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, ruleChainId, null, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) { public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) {
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) {
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, ruleChainId, null, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
//used for enqueueForTellNext //used for enqueueForTellNext
public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(),
tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), TbMsgCallback.EMPTY);
} }
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
@ -328,6 +336,11 @@ public final class TbMsg implements Serializable {
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) {
this(queueName, id, ts, internalType, type, originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, null, null, ctx, callback);
}
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID correlationId, Integer partition, TbMsgProcessingCtx ctx, TbMsgCallback callback) {
this.id = id; this.id = id;
this.queueName = queueName; this.queueName = queueName;
if (ts > 0) { if (ts > 0) {
@ -352,6 +365,8 @@ public final class TbMsg implements Serializable {
this.data = data; this.data = data;
this.ruleChainId = ruleChainId; this.ruleChainId = ruleChainId;
this.ruleNodeId = ruleNodeId; this.ruleNodeId = ruleNodeId;
this.correlationId = correlationId;
this.partition = partition;
this.ctx = ctx != null ? ctx : new TbMsgProcessingCtx(); this.ctx = ctx != null ? ctx : new TbMsgProcessingCtx();
this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY);
} }
@ -391,6 +406,14 @@ public final class TbMsg implements Serializable {
builder.setDataType(msg.getDataType().ordinal()); builder.setDataType(msg.getDataType().ordinal());
builder.setData(msg.getData()); builder.setData(msg.getData());
if (msg.getCorrelationId() != null) {
builder.setCorrelationIdMSB(msg.getCorrelationId().getMostSignificantBits());
builder.setCorrelationIdLSB(msg.getCorrelationId().getLeastSignificantBits());
}
if (msg.getPartition() != null) {
builder.setPartition(msg.getPartition());
}
builder.setCtx(msg.ctx.toProto()); builder.setCtx(msg.ctx.toProto());
return builder.build().toByteArray(); return builder.build().toByteArray();
} }
@ -403,6 +426,8 @@ public final class TbMsg implements Serializable {
CustomerId customerId = null; CustomerId customerId = null;
RuleChainId ruleChainId = null; RuleChainId ruleChainId = null;
RuleNodeId ruleNodeId = null; RuleNodeId ruleNodeId = null;
UUID correlationId = null;
Integer partition = null;
if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) { if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) {
customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB())); customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB()));
} }
@ -412,6 +437,10 @@ public final class TbMsg implements Serializable {
if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) { if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
} }
if (proto.getCorrelationIdMSB() != 0L && proto.getCorrelationIdLSB() != 0L) {
correlationId = new UUID(proto.getCorrelationIdMSB(), proto.getCorrelationIdLSB());
partition = proto.getPartition();
}
TbMsgProcessingCtx ctx; TbMsgProcessingCtx ctx;
if (proto.hasCtx()) { if (proto.hasCtx()) {
@ -423,7 +452,7 @@ public final class TbMsg implements Serializable {
TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId,
metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, correlationId, partition, ctx, callback);
} catch (InvalidProtocolBufferException e) { } catch (InvalidProtocolBufferException e) {
throw new IllegalStateException("Could not parse protobuf for TbMsg", e); throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
} }
@ -435,17 +464,17 @@ public final class TbMsg implements Serializable {
public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); this.metaData, this.dataType, this.data, ruleChainId, null, this.correlationId, this.partition, this.ctx, callback);
} }
public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.correlationId, this.partition, this.ctx, callback);
} }
public TbMsg copyWithNewCtx() { public TbMsg copyWithNewCtx() {
return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId, return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY); this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.correlationId, this.partition, this.ctx.copy(), TbMsgCallback.EMPTY);
} }
public TbMsgCallback getCallback() { public TbMsgCallback getCallback() {

6
common/message/src/main/proto/tbmsg.proto

@ -66,4 +66,8 @@ message TbMsgProto {
int64 customerIdLSB = 18; int64 customerIdLSB = 18;
TbMsgProcessingCtxProto ctx = 19; TbMsgProcessingCtxProto ctx = 19;
}
int64 correlationIdMSB = 20;
int64 correlationIdLSB = 21;
int32 partition = 22;
}

4
common/proto/src/main/proto/queue.proto

@ -96,6 +96,8 @@ message SessionInfoProto {
int64 deviceProfileIdLSB = 13; int64 deviceProfileIdLSB = 13;
int64 customerIdMSB = 14; int64 customerIdMSB = 14;
int64 customerIdLSB = 15; int64 customerIdLSB = 15;
optional int64 gatewayIdMSB = 16;
optional int64 gatewayIdLSB = 17;
} }
enum SessionEvent { enum SessionEvent {
@ -422,6 +424,7 @@ message GetQueueRoutingInfoResponseMsg {
string queueName = 5; string queueName = 5;
string queueTopic = 6; string queueTopic = 6;
int32 partitions = 7; int32 partitions = 7;
optional bool duplicateMsgToAllPartitions = 8;
} }
message QueueUpdateMsg { message QueueUpdateMsg {
@ -432,6 +435,7 @@ message QueueUpdateMsg {
string queueName = 5; string queueName = 5;
string queueTopic = 6; string queueTopic = 6;
int32 partitions = 7; int32 partitions = 7;
optional bool duplicateMsgToAllPartitions = 8;
} }
message QueueDeleteMsg { message QueueDeleteMsg {

74
common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java

@ -0,0 +1,74 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.common;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Slf4j
public class TbRuleEngineProducerService {
private final PartitionService partitionService;
public void sendToRuleEngine(TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> producer,
TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
List<TopicPartitionInfo> tpis = partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, tbMsg.getOriginator());
if (tpis.size() > 1) {
UUID correlationId = UUID.randomUUID();
for (int i = 0; i < tpis.size(); i++) {
TopicPartitionInfo tpi = tpis.get(i);
Integer partition = tpi.getPartition().orElse(null);
UUID id = i > 0 ? UUID.randomUUID() : tbMsg.getId();
tbMsg = tbMsg.toBuilder()
.id(id)
.correlationId(correlationId)
.partition(partition)
.build();
sendToRuleEngine(producer, tpi, tenantId, tbMsg, i == tpis.size() - 1 ? callback : null);
}
} else {
sendToRuleEngine(producer, tpis.get(0), tenantId, tbMsg, callback);
}
}
private void sendToRuleEngine(TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> producer, TopicPartitionInfo tpi,
TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg);
}
ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder()
.setTbMsg(TbMsg.toByteString(tbMsg))
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build();
producer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);
}
}

137
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery;
import com.google.common.hash.HashFunction; import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
@ -77,6 +78,7 @@ public class HashPartitionService implements PartitionService {
private final ConcurrentMap<QueueKey, String> partitionTopicsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<QueueKey, String> partitionTopicsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<QueueKey, Integer> partitionSizesMap = new ConcurrentHashMap<>(); private final ConcurrentMap<QueueKey, Integer> partitionSizesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<QueueKey, QueueConfig> queueConfigs = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, TenantRoutingInfo> tenantRoutingInfoMap = new ConcurrentHashMap<>(); private final ConcurrentMap<TenantId, TenantRoutingInfo> tenantRoutingInfoMap = new ConcurrentHashMap<>();
@ -132,6 +134,7 @@ public class HashPartitionService implements PartitionService {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue);
partitionTopicsMap.put(queueKey, queue.getQueueTopic()); partitionTopicsMap.put(queueKey, queue.getQueueTopic());
partitionSizesMap.put(queueKey, queue.getPartitions()); partitionSizesMap.put(queueKey, queue.getPartitions());
queueConfigs.put(queueKey, new QueueConfig(queue));
}); });
} }
@ -174,10 +177,12 @@ public class HashPartitionService implements PartitionService {
@Override @Override
public void updateQueues(List<TransportProtos.QueueUpdateMsg> queueUpdateMsgs) { public void updateQueues(List<TransportProtos.QueueUpdateMsg> queueUpdateMsgs) {
for (TransportProtos.QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { for (TransportProtos.QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) {
TenantId tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg);
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); TenantId tenantId = queueRoutingInfo.getTenantId();
partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId);
partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic());
partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions());
queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo));
if (!tenantId.isSysTenantId()) { if (!tenantId.isSysTenantId()) {
tenantRoutingInfoMap.remove(tenantId); tenantRoutingInfoMap.remove(tenantId);
} }
@ -193,9 +198,7 @@ public class HashPartitionService implements PartitionService {
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
queueKeys.forEach(queueKey -> { queueKeys.forEach(queueKey -> {
myPartitions.remove(queueKey); removeQueue(queueKey);
partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey);
evictTenantInfo(queueKey.getTenantId()); evictTenantInfo(queueKey.getTenantId());
}); });
if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) {
@ -209,14 +212,17 @@ public class HashPartitionService implements PartitionService {
List<QueueKey> queueKeys = partitionSizesMap.keySet().stream() List<QueueKey> queueKeys = partitionSizesMap.keySet().stream()
.filter(queueKey -> tenantId.equals(queueKey.getTenantId())) .filter(queueKey -> tenantId.equals(queueKey.getTenantId()))
.collect(Collectors.toList()); .collect(Collectors.toList());
queueKeys.forEach(queueKey -> { queueKeys.forEach(this::removeQueue);
myPartitions.remove(queueKey);
partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey);
});
evictTenantInfo(tenantId); evictTenantInfo(tenantId);
} }
private void removeQueue(QueueKey queueKey) {
myPartitions.remove(queueKey);
partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey);
queueConfigs.remove(queueKey);
}
@Override @Override
public boolean isManagedByCurrentService(TenantId tenantId) { public boolean isManagedByCurrentService(TenantId tenantId) {
if (serviceInfoProvider.isService(ServiceType.TB_CORE) || !serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { if (serviceInfoProvider.isService(ServiceType.TB_CORE) || !serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) {
@ -254,6 +260,64 @@ public class HashPartitionService implements PartitionService {
@Override @Override
public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) { public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) {
QueueKey queueKey = getQueueKey(serviceType, queueName, tenantId);
return resolve(queueKey, entityId);
}
@Override
public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId, Integer partition) {
QueueKey queueKey = getQueueKey(serviceType, queueName, tenantId);
if (partition != null) {
return buildTopicPartitionInfo(queueKey, partition);
} else {
return resolve(queueKey, entityId);
}
}
@Override
public TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId) {
return resolve(serviceType, null, tenantId, entityId);
}
@Override
public List<TopicPartitionInfo> resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) {
QueueKey queueKey = getQueueKey(serviceType, queueName, tenantId);
TopicPartitionInfo tpi = resolve(queueKey, entityId);
if (serviceType != ServiceType.TB_RULE_ENGINE || tpi.getPartition().isEmpty()) {
return List.of(tpi);
}
QueueConfig queueConfig = queueConfigs.get(queueKey);
if (queueConfig != null && queueConfig.isDuplicateMsgToAllPartitions()) {
int partition = tpi.getPartition().get();
Integer partitionsCount = partitionSizesMap.get(queueKey);
List<TopicPartitionInfo> partitions = new ArrayList<>(partitionsCount);
partitions.add(tpi);
for (int i = 0; i < partitionsCount; i++) {
if (i != partition) {
partitions.add(buildTopicPartitionInfo(queueKey, i, false));
}
}
return partitions;
} else {
return Collections.singletonList(tpi);
}
}
private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
Integer partitionSize = partitionSizesMap.get(queueKey);
if (partitionSize == null) {
throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing");
}
int hash = hash(entityId.getId());
int partition = Math.abs(hash % partitionSize);
return buildTopicPartitionInfo(queueKey, partition);
}
private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) {
TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId); TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId);
if (queueName == null) { if (queueName == null) {
queueName = MAIN_QUEUE_NAME; queueName = MAIN_QUEUE_NAME;
@ -270,12 +334,7 @@ public class HashPartitionService implements PartitionService {
log.warn("Using queue {} instead of isolated {} for tenant {}", queueKey, queueName, isolatedOrSystemTenantId); log.warn("Using queue {} instead of isolated {} for tenant {}", queueKey, queueName, isolatedOrSystemTenantId);
} }
} }
return resolve(queueKey, entityId); return queueKey;
}
@Override
public TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId) {
return resolve(serviceType, null, tenantId, entityId);
} }
@Override @Override
@ -288,18 +347,6 @@ public class HashPartitionService implements PartitionService {
} }
} }
private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
Integer partitionSize = partitionSizesMap.get(queueKey);
if (partitionSize == null) {
throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing");
}
int hash = hash(entityId.getId());
int partition = Math.abs(hash % partitionSize);
return buildTopicPartitionInfo(queueKey, partition);
}
@Override @Override
public synchronized void recalculatePartitions(ServiceInfo currentService, List<ServiceInfo> otherServices) { public synchronized void recalculatePartitions(ServiceInfo currentService, List<ServiceInfo> otherServices) {
log.info("Recalculating partitions"); log.info("Recalculating partitions");
@ -478,18 +525,17 @@ public class HashPartitionService implements PartitionService {
} }
private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition) { private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition) {
TopicPartitionInfo.TopicPartitionInfoBuilder tpi = TopicPartitionInfo.builder();
tpi.topic(topicService.buildTopicName(partitionTopicsMap.get(queueKey)));
tpi.partition(partition);
tpi.tenantId(queueKey.getTenantId());
List<Integer> partitions = myPartitions.get(queueKey); List<Integer> partitions = myPartitions.get(queueKey);
if (partitions != null) { return buildTopicPartitionInfo(queueKey, partition, partitions != null && partitions.contains(partition));
tpi.myPartition(partitions.contains(partition)); }
} else {
tpi.myPartition(false); private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition, boolean myPartition) {
} return TopicPartitionInfo.builder()
return tpi.build(); .topic(topicService.buildTopicName(partitionTopicsMap.get(queueKey)))
.partition(partition)
.tenantId(queueKey.getTenantId())
.myPartition(myPartition)
.build();
} }
private boolean isIsolated(ServiceType serviceType, TenantId tenantId) { private boolean isIsolated(ServiceType serviceType, TenantId tenantId) {
@ -619,4 +665,13 @@ public class HashPartitionService implements PartitionService {
return serviceInfos.stream().map(ServiceInfo::getServiceId).collect(Collectors.toList()); return serviceInfos.stream().map(ServiceInfo::getServiceId).collect(Collectors.toList());
} }
@Data
public static class QueueConfig {
private boolean duplicateMsgToAllPartitions;
public QueueConfig(QueueRoutingInfo queueRoutingInfo) {
this.duplicateMsgToAllPartitions = queueRoutingInfo.isDuplicateMsgToAllPartitions();
}
}
} }

4
common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java

@ -33,8 +33,12 @@ public interface PartitionService {
TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId);
TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId, Integer partition);
TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId); TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId);
List<TopicPartitionInfo> resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId);
boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId); boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId);
List<Integer> getMyPartitions(QueueKey queueKey); List<Integer> getMyPartitions(QueueKey queueKey);

9
common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java

@ -33,6 +33,7 @@ public class QueueRoutingInfo {
private final String queueName; private final String queueName;
private final String queueTopic; private final String queueTopic;
private final int partitions; private final int partitions;
private final boolean duplicateMsgToAllPartitions;
public QueueRoutingInfo(Queue queue) { public QueueRoutingInfo(Queue queue) {
this.tenantId = queue.getTenantId(); this.tenantId = queue.getTenantId();
@ -40,21 +41,25 @@ public class QueueRoutingInfo {
this.queueName = queue.getName(); this.queueName = queue.getName();
this.queueTopic = queue.getTopic(); this.queueTopic = queue.getTopic();
this.partitions = queue.getPartitions(); this.partitions = queue.getPartitions();
this.duplicateMsgToAllPartitions = queue.isDuplicateMsgToAllPartitions();
} }
public QueueRoutingInfo(GetQueueRoutingInfoResponseMsg routingInfo) { public QueueRoutingInfo(GetQueueRoutingInfoResponseMsg routingInfo) {
this.tenantId = new TenantId(new UUID(routingInfo.getTenantIdMSB(), routingInfo.getTenantIdLSB())); this.tenantId = TenantId.fromUUID(new UUID(routingInfo.getTenantIdMSB(), routingInfo.getTenantIdLSB()));
this.queueId = new QueueId(new UUID(routingInfo.getQueueIdMSB(), routingInfo.getQueueIdLSB())); this.queueId = new QueueId(new UUID(routingInfo.getQueueIdMSB(), routingInfo.getQueueIdLSB()));
this.queueName = routingInfo.getQueueName(); this.queueName = routingInfo.getQueueName();
this.queueTopic = routingInfo.getQueueTopic(); this.queueTopic = routingInfo.getQueueTopic();
this.partitions = routingInfo.getPartitions(); this.partitions = routingInfo.getPartitions();
this.duplicateMsgToAllPartitions = routingInfo.hasDuplicateMsgToAllPartitions() && routingInfo.getDuplicateMsgToAllPartitions();
} }
public QueueRoutingInfo(QueueUpdateMsg queueUpdateMsg) { public QueueRoutingInfo(QueueUpdateMsg queueUpdateMsg) {
this.tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); this.tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB()));
this.queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); this.queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB()));
this.queueName = queueUpdateMsg.getQueueName(); this.queueName = queueUpdateMsg.getQueueName();
this.queueTopic = queueUpdateMsg.getQueueTopic(); this.queueTopic = queueUpdateMsg.getQueueTopic();
this.partitions = queueUpdateMsg.getPartitions(); this.partitions = queueUpdateMsg.getPartitions();
this.duplicateMsgToAllPartitions = queueUpdateMsg.hasDuplicateMsgToAllPartitions() && queueUpdateMsg.getDuplicateMsgToAllPartitions();
} }
} }

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

@ -16,7 +16,6 @@
package org.thingsboard.server.queue.kafka; package org.thingsboard.server.queue.kafka;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.CreateTopicsResult; import org.apache.kafka.clients.admin.CreateTopicsResult;
import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.common.errors.TopicExistsException; import org.apache.kafka.common.errors.TopicExistsException;
@ -35,23 +34,17 @@ import java.util.concurrent.ExecutionException;
@Slf4j @Slf4j
public class TbKafkaAdmin implements TbQueueAdmin { public class TbKafkaAdmin implements TbQueueAdmin {
private final AdminClient client; private final TbKafkaSettings settings;
private final Map<String, String> topicConfigs; private final Map<String, String> topicConfigs;
private final Set<String> topics = ConcurrentHashMap.newKeySet();
private final int numPartitions; private final int numPartitions;
private volatile Set<String> topics;
private final short replicationFactor; private final short replicationFactor;
public TbKafkaAdmin(TbKafkaSettings settings, Map<String, String> topicConfigs) { public TbKafkaAdmin(TbKafkaSettings settings, Map<String, String> topicConfigs) {
client = AdminClient.create(settings.toAdminProps()); this.settings = settings;
this.topicConfigs = topicConfigs; this.topicConfigs = topicConfigs;
try {
topics.addAll(client.listTopics().names().get());
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to get all topics.", e);
}
String numPartitionsStr = topicConfigs.get(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); String numPartitionsStr = topicConfigs.get(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING);
if (numPartitionsStr != null) { if (numPartitionsStr != null) {
numPartitions = Integer.parseInt(numPartitionsStr); numPartitions = Integer.parseInt(numPartitionsStr);
@ -64,6 +57,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
@Override @Override
public void createTopicIfNotExists(String topic, String properties) { public void createTopicIfNotExists(String topic, String properties) {
Set<String> topics = getTopics();
if (topics.contains(topic)) { if (topics.contains(topic)) {
return; return;
} }
@ -86,12 +80,13 @@ public class TbKafkaAdmin implements TbQueueAdmin {
@Override @Override
public void deleteTopic(String topic) { public void deleteTopic(String topic) {
Set<String> topics = getTopics();
if (topics.contains(topic)) { if (topics.contains(topic)) {
client.deleteTopics(Collections.singletonList(topic)); settings.getAdminClient().deleteTopics(Collections.singletonList(topic));
} else { } else {
try { try {
if (client.listTopics().names().get().contains(topic)) { if (settings.getAdminClient().listTopics().names().get().contains(topic)) {
client.deleteTopics(Collections.singletonList(topic)); settings.getAdminClient().deleteTopics(Collections.singletonList(topic));
} else { } else {
log.warn("Kafka topic [{}] does not exist.", topic); log.warn("Kafka topic [{}] does not exist.", topic);
} }
@ -101,14 +96,28 @@ public class TbKafkaAdmin implements TbQueueAdmin {
} }
} }
@Override private Set<String> getTopics() {
public void destroy() { if (topics == null) {
if (client != null) { synchronized (this) {
client.close(); if (topics == null) {
topics = ConcurrentHashMap.newKeySet();
try {
topics.addAll(settings.getAdminClient().listTopics().names().get());
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to get all topics.", e);
}
}
}
} }
return topics;
} }
public CreateTopicsResult createTopic(NewTopic topic) { public CreateTopicsResult createTopic(NewTopic topic) {
return client.createTopics(Collections.singletonList(topic)); return settings.getAdminClient().createTopics(Collections.singletonList(topic));
}
@Override
public void destroy() {
} }
} }

16
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java

@ -15,11 +15,12 @@
*/ */
package org.thingsboard.server.queue.kafka; package org.thingsboard.server.queue.kafka;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.KafkaConsumer;
@ -35,8 +36,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -62,7 +61,6 @@ public class TbKafkaConsumerStatsService {
@Autowired @Autowired
private PartitionService partitionService; private PartitionService partitionService;
private AdminClient adminClient;
private Consumer<String, byte[]> consumer; private Consumer<String, byte[]> consumer;
private ScheduledExecutorService statsPrintScheduler; private ScheduledExecutorService statsPrintScheduler;
@ -71,7 +69,6 @@ public class TbKafkaConsumerStatsService {
if (!statsConfig.getEnabled()) { if (!statsConfig.getEnabled()) {
return; return;
} }
this.adminClient = AdminClient.create(kafkaSettings.toAdminProps());
this.statsPrintScheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("kafka-consumer-stats")); this.statsPrintScheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("kafka-consumer-stats"));
Properties consumerProps = kafkaSettings.toConsumerProps(null); Properties consumerProps = kafkaSettings.toConsumerProps(null);
@ -90,7 +87,7 @@ public class TbKafkaConsumerStatsService {
} }
for (String groupId : monitoredGroups) { for (String groupId : monitoredGroups) {
try { try {
Map<TopicPartition, OffsetAndMetadata> groupOffsets = adminClient.listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() Map<TopicPartition, OffsetAndMetadata> groupOffsets = kafkaSettings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata()
.get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS); .get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS);
Map<TopicPartition, Long> endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration); Map<TopicPartition, Long> endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration);
@ -115,8 +112,8 @@ public class TbKafkaConsumerStatsService {
} }
private boolean isStatsPrintRequired() { private boolean isStatsPrintRequired() {
boolean isMyRuleEnginePartition = partitionService.resolve(ServiceType.TB_RULE_ENGINE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); boolean isMyRuleEnginePartition = partitionService.isMyPartition(ServiceType.TB_RULE_ENGINE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID);
boolean isMyCorePartition = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); boolean isMyCorePartition = partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID);
return log.isInfoEnabled() && (isMyRuleEnginePartition || isMyCorePartition); return log.isInfoEnabled() && (isMyRuleEnginePartition || isMyCorePartition);
} }
@ -157,9 +154,6 @@ public class TbKafkaConsumerStatsService {
if (statsPrintScheduler != null) { if (statsPrintScheduler != null) {
statsPrintScheduler.shutdownNow(); statsPrintScheduler.shutdownNow();
} }
if (adminClient != null) {
adminClient.close();
}
if (consumer != null) { if (consumer != null) {
consumer.close(); consumer.close();
} }

35
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java

@ -19,6 +19,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerConfig;
@ -34,6 +35,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbProperty; import org.thingsboard.server.common.data.TbProperty;
import org.thingsboard.server.queue.util.PropertyUtils; import org.thingsboard.server.queue.util.PropertyUtils;
import javax.annotation.PreDestroy;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -143,13 +145,7 @@ public class TbKafkaSettings {
@Setter @Setter
private Map<String, List<TbProperty>> consumerPropertiesPerTopic = Collections.emptyMap(); private Map<String, List<TbProperty>> consumerPropertiesPerTopic = Collections.emptyMap();
public Properties toAdminProps() { private volatile AdminClient adminClient;
Properties props = toProps();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(AdminClientConfig.RETRIES_CONFIG, retries);
return props;
}
public Properties toConsumerProps(String topic) { public Properties toConsumerProps(String topic) {
Properties props = toProps(); Properties props = toProps();
@ -221,4 +217,29 @@ public class TbKafkaSettings {
} }
} }
public AdminClient getAdminClient() {
if (adminClient == null) {
synchronized (this) {
if (adminClient == null) {
adminClient = AdminClient.create(toAdminProps());
}
}
}
return adminClient;
}
protected Properties toAdminProps() {
Properties props = toProps();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(AdminClientConfig.RETRIES_CONFIG, retries);
return props;
}
@PreDestroy
private void destroy() {
if (adminClient != null) {
adminClient.close();
}
}
} }

4
common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java

@ -49,10 +49,10 @@ class PropertyUtilsTest {
@Test @Test
void givenKafkaTopicProperties_whenGetConfig_thenReturnMappedValues() { void givenKafkaTopicProperties_whenGetConfig_thenReturnMappedValues() {
assertThat(PropertyUtils.getProps("retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1")) assertThat(PropertyUtils.getProps("retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1"))
.isEqualTo(Map.of( .isEqualTo(Map.of(
"retention.ms", "604800000", "retention.ms", "604800000",
"segment.bytes", "26214400", "segment.bytes", "52428800",
"retention.bytes", "1048576000", "retention.bytes", "1048576000",
"partitions", "1", "partitions", "1",
"min.insync.replicas", "1" "min.insync.replicas", "1"

9
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -61,7 +61,6 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT;
@ -417,14 +416,14 @@ public class LwM2mClient {
private static Set<ContentFormat> clientSupportContentFormat(Registration registration) { private static Set<ContentFormat> clientSupportContentFormat(Registration registration) {
Set<ContentFormat> contentFormats = new HashSet<>(); Set<ContentFormat> contentFormats = new HashSet<>();
contentFormats.add(ContentFormat.DEFAULT);
Attribute ct = Arrays.stream(registration.getObjectLinks()) Attribute ct = Arrays.stream(registration.getObjectLinks())
.filter(link -> link.getUriReference().equals("/")) .filter(link -> link.getUriReference().equals("/"))
.findFirst() .findFirst()
.map(link -> link.getAttributes().get("ct")).orElse(null); .map(link -> link.getAttributes().get("ct")).orElse(null);
if (ct != null) { if (ct != null && ct.getValue() instanceof Collection<?>) {
Set codes = Stream.of(ct.getValue()).collect(Collectors.toSet()); contentFormats.addAll((Collection<? extends ContentFormat>) ct.getValue());
contentFormats.addAll(codes); } else {
contentFormats.add(ContentFormat.DEFAULT);
} }
return contentFormats; return contentFormats;
} }

51
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java

@ -98,7 +98,6 @@ import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -742,16 +741,18 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
private static ContentFormat getRequestContentFormat(LwM2mClient client, String versionedId, LwM2mModelProvider modelProvider) { private static ContentFormat getRequestContentFormat(LwM2mClient client, String versionedId, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(versionedId)); LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(versionedId));
if (pathIds.isResource() || pathIds.isResourceInstance()) { if (pathIds.isResourceInstance() || pathIds.isResource()) {
ResourceModel resourceModel = client.getResourceModel(versionedId, modelProvider); ResourceModel resourceModel = client.getResourceModel(versionedId, modelProvider);
if (resourceModel != null && (pathIds.isResourceInstance() || (pathIds.isResource() && !resourceModel.multiple))) { if (resourceModel != null && (pathIds.isResourceInstance() || (pathIds.isResource() && !resourceModel.multiple))) {
ContentFormat[] desiredFormats;
if (OBJLNK.equals(resourceModel.type)) { if (OBJLNK.equals(resourceModel.type)) {
return ContentFormat.LINK; desiredFormats = new ContentFormat[]{ContentFormat.LINK, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON};
} else if (OPAQUE.equals(resourceModel.type)) { } else if (OPAQUE.equals(resourceModel.type)) {
return ContentFormat.OPAQUE; desiredFormats = new ContentFormat[]{ContentFormat.OPAQUE, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON};
} else { } else {
return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON); desiredFormats = new ContentFormat[]{ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON};
} }
return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), desiredFormats);
} else { } else {
return getContentFormatForComplex(client); return getContentFormatForComplex(client);
} }
@ -762,9 +763,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
private static ContentFormat getContentFormatForComplex(LwM2mClient client) { private static ContentFormat getContentFormatForComplex(LwM2mClient client) {
if (LwM2m.LwM2mVersion.V1_0.equals(client.getRegistration().getLwM2mVersion())) { if (LwM2m.LwM2mVersion.V1_0.equals(client.getRegistration().getLwM2mVersion())) {
return ContentFormat.TLV; return client.getDefaultContentFormat();
} else if (LwM2m.LwM2mVersion.V1_1.equals(client.getRegistration().getLwM2mVersion())) { } else if (LwM2m.LwM2mVersion.V1_1.equals(client.getRegistration().getLwM2mVersion())) {
ContentFormat result = findFirstContentFormatForComp(client.getClientSupportContentFormats(), null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON); ContentFormat result = findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON);
if (result != null) { if (result != null) {
return result; return result;
} else { } else {
@ -783,37 +784,21 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
} }
} }
private ContentFormat findFirstContentFormatForComposite (Set<?> clientSupportContentFormats) { private ContentFormat findFirstContentFormatForComposite (Set<ContentFormat> clientSupportContentFormats) {
ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_JSON, ContentFormat.SENML_CBOR); ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON);
if (contentFormat != null) { if (contentFormat != null) {
return contentFormat; return contentFormat;
} else { } else {
throw new RuntimeException("This device does not support Composite Operation"); throw new RuntimeException("This device does not support Composite Operation");
} }
} }
private static ContentFormat findFirstContentFormatForComp(Set<?> clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) { private static ContentFormat findFirstContentFormatForComp(Set<ContentFormat> clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) {
AtomicReference<ContentFormat> compositeContentFormat = new AtomicReference<>(); List desiredFormatsList = Arrays.asList(desiredFormats);
clientSupportContentFormats.forEach(c -> { for (ContentFormat c : clientSupportContentFormats) {
if (c instanceof Collection) { if (desiredFormatsList.contains(c)) {
for (ContentFormat contentFormat : desiredFormats) { return c;
if (((Collection<?>) c).contains(contentFormat)) {
compositeContentFormat.set(contentFormat);
break;
}
}
} else if (compositeContentFormat.get() == null && c instanceof ContentFormat) {
compositeContentFormat.set(Arrays.stream(desiredFormats).filter(f -> f.equals(c)).findFirst().get());
}
});
return compositeContentFormat.get() != null ? compositeContentFormat.get() : defaultValue;
}
private static <T> boolean containsObserveComposite(List<T> l1, List<T> l2) {
for (T elem : l1) {
if (l2.contains(elem)) {
return true;
} }
} }
return false; return defaultValue;
} }
} }

13
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -223,7 +223,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.debug("[{}] Cleanup RPC awaiting ack map due to session close!", sessionId); log.debug("[{}] Cleanup RPC awaiting ack map due to session close!", sessionId);
rpcAwaitingAck.clear(); rpcAwaitingAck.clear();
} }
if (ctx.channel() != null && ctx.channel().isOpen()) {
if (ctx.channel() == null) {
log.debug("[{}] Channel is null, closing ctx...", sessionId);
ctx.close();
} else if (ctx.channel().isOpen()) {
if (msg != null && MqttVersion.MQTT_5 == deviceSessionCtx.getMqttVersion()) { if (msg != null && MqttVersion.MQTT_5 == deviceSessionCtx.getMqttVersion()) {
ChannelFuture channelFuture = ctx.writeAndFlush(msg).addListener(future -> ctx.close()); ChannelFuture channelFuture = ctx.writeAndFlush(msg).addListener(future -> ctx.close());
scheduler.schedule(() -> { scheduler.schedule(() -> {
@ -236,12 +240,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
ctx.close(); ctx.close();
} }
} else { } else {
if (ctx.channel() != null) { log.debug("[{}] Channel is already closed!", sessionId);
log.debug("[{}] Channel is already closed!", sessionId);
} else {
log.debug("[{}] Channel is null, closing ctx...", sessionId);
ctx.close();
}
} }
} }

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java

@ -59,6 +59,8 @@ public abstract class AbstractGatewayDeviceSessionContext<T extends AbstractGate
.setDeviceType(deviceInfo.getDeviceType()) .setDeviceType(deviceInfo.getDeviceType())
.setGwSessionIdMSB(parent.getSessionId().getMostSignificantBits()) .setGwSessionIdMSB(parent.getSessionId().getMostSignificantBits())
.setGwSessionIdLSB(parent.getSessionId().getLeastSignificantBits()) .setGwSessionIdLSB(parent.getSessionId().getLeastSignificantBits())
.setGatewayIdMSB(parent.gateway.getDeviceId().getId().getMostSignificantBits())
.setGatewayIdLSB(parent.gateway.getDeviceId().getId().getLeastSignificantBits())
.setDeviceProfileIdMSB(deviceInfo.getDeviceProfileId().getId().getMostSignificantBits()) .setDeviceProfileIdMSB(deviceInfo.getDeviceProfileId().getId().getMostSignificantBits())
.setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits())
.build()); .build());

445
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -15,9 +15,7 @@
*/ */
package org.thingsboard.server.transport.mqtt.session; package org.thingsboard.server.transport.mqtt.session;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
@ -35,12 +33,13 @@ import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttReasonCodes; import io.netty.handler.codec.mqtt.MqttReasonCodes;
import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.codec.mqtt.MqttVersion;
import jakarta.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.adaptor.ProtoConverter;
@ -78,6 +77,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE;
@ -97,10 +97,16 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; private static final String CAN_T_PARSE_VALUE = "Can't parse value: ";
private static final String DEVICE_PROPERTY = "device"; private static final String DEVICE_PROPERTY = "device";
public static final String TELEMETRY = "telemetry";
public static final String CLAIMING = "claiming";
public static final String ATTRIBUTE = "attribute";
public static final String RPC_RESPONSE = "Rpc response";
public static final String ATTRIBUTES_REQUEST = "attributes request";
protected final MqttTransportContext context; protected final MqttTransportContext context;
protected final TransportService transportService; protected final TransportService transportService;
protected final TransportDeviceInfo gateway; protected final TransportDeviceInfo gateway;
@Getter
protected final UUID sessionId; protected final UUID sessionId;
private final ConcurrentMap<String, Lock> deviceCreationLockMap; private final ConcurrentMap<String, Lock> deviceCreationLockMap;
private final ConcurrentMap<String, T> devices; private final ConcurrentMap<String, T> devices;
@ -195,10 +201,6 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
return context.getNodeId(); return context.getNodeId();
} }
public UUID getSessionId() {
return sessionId;
}
public MqttTransportAdaptor getPayloadAdaptor() { public MqttTransportAdaptor getPayloadAdaptor() {
return deviceSessionCtx.getPayloadAdaptor(); return deviceSessionCtx.getPayloadAdaptor();
} }
@ -226,18 +228,12 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) {
log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback<>() { process(onDeviceConnect(deviceName, deviceType),
@Override result -> {
public void onSuccess(@Nullable T result) { ack(msg, MqttReasonCodes.PubAck.SUCCESS);
ack(msg, MqttReasonCodes.PubAck.SUCCESS); log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); },
} t -> logDeviceCreationError(t, deviceName));
@Override
public void onFailure(Throwable t) {
logDeviceCreationError(t, deviceName);
}
}, context.getExecutor());
} }
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) { public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) {
@ -370,34 +366,25 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { protected void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException {
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload);
if (json.isJsonObject()) { validateJsonObject(json);
JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry<String, JsonElement> deviceEntry : json.getAsJsonObject().entrySet()) {
for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { if (!deviceEntry.getValue().isJsonArray()) {
String deviceName = deviceEntry.getKey(); log.warn("{}[{}]", CAN_T_PARSE_VALUE, json);
Futures.addCallback(checkDeviceConnected(deviceName), continue;
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable T deviceCtx) {
if (!deviceEntry.getValue().isJsonArray()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray());
processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e);
channel.close();
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device telemetry command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
} }
} else { String deviceName = deviceEntry.getKey();
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId),
t -> failedToProcessLog(deviceName, TELEMETRY, t));
}
}
private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId) {
try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(msg.getAsJsonArray());
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e);
ackOrClose(msgId);
} }
} }
@ -405,40 +392,29 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
try { try {
TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload));
List<TransportApiProtos.TelemetryMsg> deviceMsgList = telemetryMsgProto.getMsgList(); List<TransportApiProtos.TelemetryMsg> deviceMsgList = telemetryMsgProto.getMsgList();
if (!CollectionUtils.isEmpty(deviceMsgList)) { if (CollectionUtils.isEmpty(deviceMsgList)) {
deviceMsgList.forEach(telemetryMsg -> {
String deviceName = checkDeviceName(telemetryMsg.getDeviceName());
Futures.addCallback(checkDeviceConnected(deviceName),
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable T deviceCtx) {
TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg();
try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray());
processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e);
channel.close();
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device telemetry command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
});
} else {
log.debug("[{}][{}][{}] Devices telemetry messages is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); log.debug("[{}][{}][{}] Devices telemetry messages is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId);
throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]");
} }
deviceMsgList.forEach(telemetryMsg -> {
String deviceName = checkDeviceName(telemetryMsg.getDeviceName());
process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId),
t -> failedToProcessLog(deviceName, TELEMETRY, t));
});
} catch (RuntimeException | InvalidProtocolBufferException e) { } catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
} }
public void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId) {
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray());
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e);
ackOrClose(msgId);
}
} }
public TransportProtos.PostTelemetryMsg postTelemetryMsgCreated(TransportProtos.KeyValueProto keyValueProto, long ts) { public TransportProtos.PostTelemetryMsg postTelemetryMsgCreated(TransportProtos.KeyValueProto keyValueProto, long ts) {
@ -454,34 +430,27 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException {
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload);
if (json.isJsonObject()) { validateJsonObject(json);
JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry<String, JsonElement> deviceEntry : json.getAsJsonObject().entrySet()) {
for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { if (!deviceEntry.getValue().isJsonObject()) {
String deviceName = deviceEntry.getKey(); log.warn("{}[{}]", CAN_T_PARSE_VALUE, json);
Futures.addCallback(checkDeviceConnected(deviceName), continue;
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable T deviceCtx) {
if (!deviceEntry.getValue().isJsonObject()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
try {
DeviceId deviceId = deviceCtx.getDeviceId();
TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue());
processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e);
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device claiming command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
} }
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); String deviceName = deviceEntry.getKey();
process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId),
t -> failedToProcessLog(deviceName, CLAIMING, t));
}
}
private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId) {
try {
DeviceId deviceId = deviceCtx.getDeviceId();
TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, claimRequest);
transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e);
ackOrClose(msgId);
} }
} }
@ -489,70 +458,54 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
try { try {
TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload));
List<TransportApiProtos.ClaimDeviceMsg> claimMsgList = claimMsgProto.getMsgList(); List<TransportApiProtos.ClaimDeviceMsg> claimMsgList = claimMsgProto.getMsgList();
if (!CollectionUtils.isEmpty(claimMsgList)) { if (CollectionUtils.isEmpty(claimMsgList)) {
claimMsgList.forEach(claimDeviceMsg -> {
String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName());
Futures.addCallback(checkDeviceConnected(deviceName),
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable T deviceCtx) {
TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest();
if (claimRequest == null) {
throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!");
}
try {
DeviceId deviceId = deviceCtx.getDeviceId();
TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray());
processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e);
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device claiming command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
});
} else {
log.debug("[{}][{}][{}] Devices claim messages is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); log.debug("[{}][{}][{}] Devices claim messages is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId);
throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]");
} }
claimMsgList.forEach(claimDeviceMsg -> {
String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName());
process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId),
t -> failedToProcessLog(deviceName, CLAIMING, t));
});
} catch (RuntimeException | InvalidProtocolBufferException e) { } catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
} }
private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId) {
transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); try {
DeviceId deviceId = deviceCtx.getDeviceId();
TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray());
transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e);
ackOrClose(msgId);
}
} }
private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException {
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload);
if (json.isJsonObject()) { validateJsonObject(json);
JsonObject jsonObj = json.getAsJsonObject(); for (Map.Entry<String, JsonElement> deviceEntry : json.getAsJsonObject().entrySet()) {
for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { if (!deviceEntry.getValue().isJsonObject()) {
String deviceName = deviceEntry.getKey(); log.warn("{}[{}]", CAN_T_PARSE_VALUE, json);
Futures.addCallback(checkDeviceConnected(deviceName), continue;
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable T deviceCtx) {
if (!deviceEntry.getValue().isJsonObject()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject());
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
} }
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); String deviceName = deviceEntry.getKey();
process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId),
t -> failedToProcessLog(deviceName, ATTRIBUTE, t));
}
}
private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId) {
try {
TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(msg.getAsJsonObject());
transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e);
ackOrClose(msgId);
} }
} }
@ -560,66 +513,50 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
try { try {
TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload));
List<TransportApiProtos.AttributesMsg> attributesMsgList = attributesMsgProto.getMsgList(); List<TransportApiProtos.AttributesMsg> attributesMsgList = attributesMsgProto.getMsgList();
if (!CollectionUtils.isEmpty(attributesMsgList)) { if (CollectionUtils.isEmpty(attributesMsgList)) {
attributesMsgList.forEach(attributesMsg -> {
String deviceName = checkDeviceName(attributesMsg.getDeviceName());
Futures.addCallback(checkDeviceConnected(deviceName),
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable T deviceCtx) {
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg();
if (kvListProto == null) {
throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!");
}
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e);
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
});
} else {
log.debug("[{}][{}][{}] Devices attributes keys list is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); log.debug("[{}][{}][{}] Devices attributes keys list is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId);
throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]");
} }
attributesMsgList.forEach(attributesMsg -> {
String deviceName = checkDeviceName(attributesMsg.getDeviceName());
process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId),
t -> failedToProcessLog(deviceName, ATTRIBUTE, t));
});
} catch (RuntimeException | InvalidProtocolBufferException e) { } catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
} }
protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId) {
transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e);
ackOrClose(msgId);
}
} }
private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException {
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload());
if (json.isJsonObject()) { validateJsonObject(json);
JsonObject jsonObj = json.getAsJsonObject(); JsonObject jsonObj = json.getAsJsonObject();
int requestId = jsonObj.get("id").getAsInt(); int requestId = jsonObj.get("id").getAsInt();
String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
boolean clientScope = jsonObj.get("client").getAsBoolean(); boolean clientScope = jsonObj.get("client").getAsBoolean();
Set<String> keys; Set<String> keys;
if (jsonObj.has("key")) { if (jsonObj.has("key")) {
keys = Collections.singleton(jsonObj.get("key").getAsString()); keys = Collections.singleton(jsonObj.get("key").getAsString());
} else {
JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
keys = new HashSet<>();
for (JsonElement keyObj : keysArray) {
keys.add(keyObj.getAsString());
}
}
TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys);
processGetAttributeRequestMessage(msg, deviceName, requestMsg);
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
keys = new HashSet<>();
for (JsonElement keyObj : keysArray) {
keys.add(keyObj.getAsString());
}
} }
TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys);
processGetAttributeRequestMessage(msg, deviceName, requestMsg);
} }
private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException {
@ -639,26 +576,16 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException {
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload);
if (json.isJsonObject()) { validateJsonObject(json);
JsonObject jsonObj = json.getAsJsonObject(); JsonObject jsonObj = json.getAsJsonObject();
String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
Futures.addCallback(checkDeviceConnected(deviceName), Integer requestId = jsonObj.get("id").getAsInt();
new FutureCallback<>() { String data = jsonObj.get("data").toString();
@Override onDeviceRpcResponse(requestId, data, deviceName, msgId);
public void onSuccess(@Nullable T deviceCtx) { }
Integer requestId = jsonObj.get("id").getAsInt();
String data = jsonObj.get("data").toString();
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestId).setPayload(data).build();
processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId);
}
@Override private static void validateJsonObject(JsonElement json) {
public void onFailure(Throwable t) { if (!json.isJsonObject()) {
log.debug("[{}][{}][{}] Failed to process device Rpc response command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
} }
} }
@ -667,46 +594,36 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
try { try {
TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload));
String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName());
Futures.addCallback(checkDeviceConnected(deviceName), Integer requestId = gatewayRpcResponseMsg.getId();
new FutureCallback<>() { String data = gatewayRpcResponseMsg.getData();
@Override onDeviceRpcResponse(requestId, data, deviceName, msgId);
public void onSuccess(@Nullable T deviceCtx) {
Integer requestId = gatewayRpcResponseMsg.getId();
String data = gatewayRpcResponseMsg.getData();
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestId).setPayload(data).build();
processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId);
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}][{}][{}] Failed to process device Rpc response command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
} catch (RuntimeException | InvalidProtocolBufferException e) { } catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
} }
private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { private void onDeviceRpcResponse(Integer requestId, String data, String deviceName, int msgId) {
process(deviceName, deviceCtx -> processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId),
t -> failedToProcessLog(deviceName, RPC_RESPONSE, t));
}
private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, int msgId) {
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestId).setPayload(data).build();
transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg));
} }
private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) {
int msgId = getMsgId(mqttMsg); int msgId = getMsgId(mqttMsg);
Futures.addCallback(checkDeviceConnected(deviceName), process(deviceName, deviceCtx -> processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId),
new FutureCallback<>() { t -> {
@Override failedToProcessLog(deviceName, ATTRIBUTES_REQUEST, t);
public void onSuccess(@Nullable T deviceCtx) { ack(mqttMsg, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR);
transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); });
} }
@Override private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, String deviceName, int msgId) {
public void onFailure(Throwable t) { transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg));
ack(mqttMsg, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR);
log.debug("[{}][{}][{}] Failed to process device attributes request command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t);
}
}, context.getExecutor());
} }
private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set<String> keys) { private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set<String> keys) {
@ -721,16 +638,6 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
return result.build(); return result.build();
} }
protected ListenableFuture<T> checkDeviceConnected(String deviceName) {
T ctx = devices.get(deviceName);
if (ctx == null) {
log.debug("[{}][{}][{}] Missing device [{}] for the gateway session", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE);
} else {
return Futures.immediateFuture(ctx);
}
}
protected String checkDeviceName(String deviceName) { protected String checkDeviceName(String deviceName) {
if (StringUtils.isEmpty(deviceName)) { if (StringUtils.isEmpty(deviceName)) {
throw new RuntimeException("Device name is empty!"); throw new RuntimeException("Device name is empty!");
@ -758,11 +665,23 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void ack(MqttPublishMessage msg, MqttReasonCodes.PubAck returnCode) { protected void ack(MqttPublishMessage msg, MqttReasonCodes.PubAck returnCode) {
int msgId = getMsgId(msg); int msgId = getMsgId(msg);
ack(msgId, returnCode);
}
protected void ack(int msgId, MqttReasonCodes.PubAck returnCode) {
if (msgId > 0) { if (msgId > 0) {
writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode.byteValue())); writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode.byteValue()));
} }
} }
protected void ackOrClose(int msgId) {
if (MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) {
ack(msgId, MqttReasonCodes.PubAck.PAYLOAD_FORMAT_INVALID);
} else {
channel.close();
}
}
private void deregisterSession(String deviceName, MqttDeviceAwareSessionContext deviceSessionCtx) { private void deregisterSession(String deviceName, MqttDeviceAwareSessionContext deviceSessionCtx) {
if (this.deviceSessionCtx.isSparkplug()) { if (this.deviceSessionCtx.isSparkplug()) {
sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(), sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(),
@ -809,6 +728,24 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
}; };
} }
protected void process(String deviceName, Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
ListenableFuture<T> deviceCtxFuture = onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE);
process(deviceCtxFuture, onSuccess, onFailure);
}
@SneakyThrows
protected <T> void process(ListenableFuture<T> deviceCtxFuture, Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
if (deviceCtxFuture.isDone()) {
onSuccess.accept(deviceCtxFuture.get());
} else {
DonAsynchron.withCallback(deviceCtxFuture, onSuccess, onFailure, context.getExecutor());
}
}
protected void failedToProcessLog(String deviceName, String msgType, Throwable t) {
log.debug("[{}][{}][{}] Failed to process device {} command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msgType, deviceName, t);
}
private void closeDeviceSession(String deviceName, MqttReasonCodes.Disconnect returnCode) { private void closeDeviceSession(String deviceName, MqttReasonCodes.Disconnect returnCode) {
try { try {

103
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.transport.mqtt.session; package org.thingsboard.server.transport.mqtt.session;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -26,15 +25,16 @@ import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttTopicSubscription; import io.netty.handler.codec.mqtt.MqttTopicSubscription;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.adaptor.AdaptorException;
import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.adaptor.ProtoConverter; import org.thingsboard.server.common.adaptor.ProtoConverter;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
@ -43,7 +43,6 @@ import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic;
import jakarta.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -51,7 +50,6 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE;
@ -68,7 +66,9 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopi
@Slf4j @Slf4j
public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> { public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> {
@Getter
private final SparkplugTopic sparkplugTopicNode; private final SparkplugTopic sparkplugTopicNode;
@Getter
private final Map<String, SparkplugBProto.Payload.Metric> nodeBirthMetrics; private final Map<String, SparkplugBProto.Payload.Metric> nodeBirthMetrics;
private final MqttTransportHandler parent; private final MqttTransportHandler parent;
@ -85,10 +85,6 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
.collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric)));
} }
public Map<String, SparkplugBProto.Payload.Metric> getNodeBirthMetrics() {
return this.nodeBirthMetrics;
}
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx;
byte[] bytes = getBytes(inbound.payload()); byte[] bytes = getBytes(inbound.payload());
@ -135,69 +131,46 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
} }
public void onDeviceTelemetryProto(ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture, public void onDeviceTelemetryProto(ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture,
int msgId, List<TransportProtos.PostTelemetryMsg> postTelemetryMsgList, String deviceName) throws AdaptorException { int msgId, List<TransportProtos.PostTelemetryMsg> postTelemetryMsgList, String deviceName) {
try { process(contextListenableFuture, deviceCtx -> {
int finalMsgId = msgId; for (TransportProtos.PostTelemetryMsg telemetryMsg : postTelemetryMsgList) {
postTelemetryMsgList.forEach(telemetryMsg -> { try {
Futures.addCallback(contextListenableFuture, processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId);
new FutureCallback<>() { } catch (Throwable e) {
@Override log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e);
public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { ackOrClose(msgId);
try { }
processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, finalMsgId); }
} catch (Throwable e) { },
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); t -> log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t));
channel.close();
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t);
}
}, context.getExecutor());
});
} catch (RuntimeException e) {
throw new AdaptorException(e);
}
} }
private void onDeviceAttributesProto(ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture, int msgId, private void onDeviceAttributesProto(ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture, int msgId,
List<TransportApiProtos.AttributesMsg> attributesMsgList, String deviceName) throws AdaptorException { List<TransportApiProtos.AttributesMsg> attributesMsgList, String deviceName) throws AdaptorException {
try { try {
if (!CollectionUtils.isEmpty(attributesMsgList)) { if (CollectionUtils.isEmpty(attributesMsgList)) {
attributesMsgList.forEach(attributesMsg -> {
Futures.addCallback(contextListenableFuture,
new FutureCallback<>() {
@Override
public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) {
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg();
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e);
}
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t);
}
}, context.getExecutor());
});
} else {
log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId());
} }
process(contextListenableFuture, deviceCtx -> {
for (TransportApiProtos.AttributesMsg attributesMsg : attributesMsgList) {
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg();
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable e) {
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e);
}
}
},
t -> log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t));
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new AdaptorException(e); throw new AdaptorException(e);
} }
} }
public void handleSparkplugSubscribeMsg(List<Integer> grantedQoSList, MqttTopicSubscription subscription, public void handleSparkplugSubscribeMsg(List<Integer> grantedQoSList, MqttTopicSubscription subscription,
MqttQoS reqQoS) throws ThingsboardException, AdaptorException, MqttQoS reqQoS) throws ThingsboardException {
ExecutionException, InterruptedException { SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicFilter());
SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicName());
if (sparkplugTopic.getGroupId() == null) { if (sparkplugTopic.getGroupId() == null) {
// TODO SUBSCRIBE NameSpace // TODO SUBSCRIBE NameSpace
} else if (sparkplugTopic.getType() == null) { } else if (sparkplugTopic.getType() == null) {
@ -238,9 +211,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
String key = "bdSeq".equals(protoMetric.getName()) ? String key = "bdSeq".equals(protoMetric.getName()) ?
topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); topicTypeName + " " + protoMetric.getName() : protoMetric.getName();
Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric);
if (keyValueProtoOpt.isPresent()) { keyValueProtoOpt.ifPresent(kvProto -> msgs.add(postTelemetryMsgCreated(kvProto, ts)));
msgs.add(postTelemetryMsgCreated(keyValueProtoOpt.get(), ts));
}
} }
} }
@ -304,10 +275,6 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
return Optional.empty(); return Optional.empty();
} }
public SparkplugTopic getSparkplugTopicNode() {
return this.sparkplugTopicNode;
}
public Optional<MqttPublishMessage> createSparkplugMqttPublishMsg(TransportProtos.TsKvProto tsKvProto, public Optional<MqttPublishMessage> createSparkplugMqttPublishMsg(TransportProtos.TsKvProto tsKvProto,
String sparkplugTopic, String sparkplugTopic,
SparkplugBProto.Payload.Metric metricBirth) { SparkplugBProto.Payload.Metric metricBirth) {
@ -328,7 +295,6 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
} catch (Exception e) { } catch (Exception e) {
log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes response to MQTT sparkplug msg", log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes response to MQTT sparkplug msg",
deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, e); deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, e);
return Optional.empty();
} }
return Optional.empty(); return Optional.empty();
} }
@ -350,5 +316,4 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
parent.sendSuccessRpcResponse(sessionInfo, requestId, result, successMsg); parent.sendSuccessRpcResponse(sessionInfo, requestId, result, successMsg);
} }
} }

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

Loading…
Cancel
Save