Browse Source

Merge with master and resolve conflict

pull/10209/head
Artem Dzhereleiko 3 years ago
parent
commit
8d389437e8
  1. 5
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 3
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  3. 6
      application/src/main/data/json/system/widget_types/action_button.json
  4. 7
      application/src/main/data/json/system/widget_types/command_button.json
  5. 36
      application/src/main/data/json/system/widget_types/power_button.json
  6. 3
      application/src/main/data/json/system/widget_types/progress_bar.json
  7. 3
      application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json
  8. 7
      application/src/main/data/json/system/widget_types/single_switch.json
  9. 39
      application/src/main/data/json/system/widget_types/slider.json
  10. 36
      application/src/main/data/json/tenant/dashboards/gateways.json
  11. 9
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  12. 14
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  13. 65
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  14. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  15. 2
      application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
  16. 2
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  17. 2
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  18. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  19. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  20. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  21. 50
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  22. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
  23. 115
      application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java
  24. 16
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  25. 2
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  26. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  27. 62
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java
  28. 60
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java
  29. 64
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  30. 93
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  31. 71
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  32. 28
      application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java
  33. 1
      application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java
  34. 12
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java
  35. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  36. 128
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  37. 196
      application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java
  38. 14
      application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java
  39. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
  40. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  41. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java
  42. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java
  43. 28
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  44. 2
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
  45. 8
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  46. 38
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java
  47. 6
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
  48. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
  49. 21
      application/src/main/resources/thingsboard.yml
  50. 20
      application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
  51. 37
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  52. 23
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  53. 7
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  54. 6
      application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
  55. 87
      application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java
  56. 4
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
  57. 16
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  58. 2
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  59. 9
      application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java
  60. 532
      application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java
  61. 420
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  62. 266
      application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java
  63. 4
      common/cache/pom.xml
  64. 14
      common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java
  65. 2
      common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java
  66. 25
      common/cache/src/main/java/org/thingsboard/server/cache/limits/TenantProfileProvider.java
  67. 8
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java
  68. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  69. 2
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  70. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  71. 66
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.java
  72. 66
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.java
  73. 63
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  74. 62
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  75. 39
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java
  76. 44
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java
  77. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java
  78. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java
  79. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  80. 3
      common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java
  81. 12
      common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java
  82. 7
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  83. 39
      common/proto/src/main/proto/queue.proto
  84. 47
      common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java
  85. 57
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  86. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
  87. 15
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
  88. 5
      common/transport/lwm2m/pom.xml
  89. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java
  90. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java
  91. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  92. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  93. 22
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java
  94. 63
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDes.java
  95. 105
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java
  96. 61
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigTest.java
  97. 109
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportServiceTest.java
  98. 265
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStoreTest.java
  99. 77
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDesTest.java
  100. 2
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

5
application/src/main/data/json/system/widget_bundles/buttons.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -9,6 +9,9 @@
}, },
"widgetTypeFqns": [ "widgetTypeFqns": [
"single_switch", "single_switch",
"command_button",
"power_button",
"slider",
"control_widgets.switch_control", "control_widgets.switch_control",
"control_widgets.slide_toggle_control", "control_widgets.slide_toggle_control",
"control_widgets.round_switch", "control_widgets.round_switch",

6
application/src/main/data/json/system/widget_types/action_button.json

File diff suppressed because one or more lines are too long

7
application/src/main/data/json/system/widget_types/command_button.json

File diff suppressed because one or more lines are too long

36
application/src/main/data/json/system/widget_types/power_button.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/progress_bar.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json

File diff suppressed because one or more lines are too long

7
application/src/main/data/json/system/widget_types/single_switch.json

File diff suppressed because one or more lines are too long

39
application/src/main/data/json/system/widget_types/slider.json

File diff suppressed because one or more lines are too long

36
application/src/main/data/json/tenant/dashboards/gateways.json

@ -2317,7 +2317,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "45e4507d-3adc-bb31-8b2b-1ba09bbd56ac" "id": "45e4507d-3adc-bb31-8b2b-1ba09bbd56ac"
@ -2484,7 +2484,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "852eccce-98eb-24db-c783-bdd62566f906" "id": "852eccce-98eb-24db-c783-bdd62566f906"
@ -2650,7 +2650,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "3c31ba62-e760-2bea-4c8d-d32784a86c24" "id": "3c31ba62-e760-2bea-4c8d-d32784a86c24"
@ -2816,7 +2816,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "4b55ea81-93bf-4206-9166-3e0bdc1dd9f3" "id": "4b55ea81-93bf-4206-9166-3e0bdc1dd9f3"
@ -2982,7 +2982,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "babf88d0-a118-e2b5-f10e-3a5970c8a65b" "id": "babf88d0-a118-e2b5-f10e-3a5970c8a65b"
@ -3148,7 +3148,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "94de7690-f91d-b032-6771-85af99abd749" "id": "94de7690-f91d-b032-6771-85af99abd749"
@ -3314,7 +3314,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "18414f44-1c65-536a-14de-eaf21a7d56bd" "id": "18414f44-1c65-536a-14de-eaf21a7d56bd"
@ -3480,7 +3480,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "794974da-c9d2-a9f7-be47-c9eb642094e8" "id": "794974da-c9d2-a9f7-be47-c9eb642094e8"
@ -3646,7 +3646,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "2add705b-3e53-8559-8126-380cac686fb0" "id": "2add705b-3e53-8559-8126-380cac686fb0"
@ -3812,7 +3812,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "7e1ba820-9992-d52a-579b-20485abb3926" "id": "7e1ba820-9992-d52a-579b-20485abb3926"
@ -3978,7 +3978,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "91af27c1-b37c-2276-6022-a332e41b2b33" "id": "91af27c1-b37c-2276-6022-a332e41b2b33"
@ -4144,7 +4144,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "26cf8696-054b-13ec-7984-6fc5df20e6f1" "id": "26cf8696-054b-13ec-7984-6fc5df20e6f1"
@ -4310,7 +4310,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "1dcfaf24-32be-cd19-62d6-86d12cc6a7ef" "id": "1dcfaf24-32be-cd19-62d6-86d12cc6a7ef"
@ -4476,7 +4476,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "ad2bc817-f3c4-150c-4672-8fe0c38aee8d" "id": "ad2bc817-f3c4-150c-4672-8fe0c38aee8d"
@ -4642,7 +4642,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "d1ad84cd-bd9c-4dca-e4a0-f444ae8598bd" "id": "d1ad84cd-bd9c-4dca-e4a0-f444ae8598bd"
@ -4808,7 +4808,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "bf80eef9-b879-9a08-40a4-488dbdefa125" "id": "bf80eef9-b879-9a08-40a4-488dbdefa125"
@ -4974,7 +4974,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "b5a406b3-cc0a-8a09-9aec-3f8befae5fb8" "id": "b5a406b3-cc0a-8a09-9aec-3f8befae5fb8"
@ -5140,7 +5140,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "ec1dfba3-4b43-2491-8948-f602337f8a3b" "id": "ec1dfba3-4b43-2491-8948-f602337f8a3b"

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

@ -31,6 +31,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -203,6 +204,10 @@ public class ActorSystemContext {
@Getter @Getter
private DeviceCredentialsService deviceCredentialsService; private DeviceCredentialsService deviceCredentialsService;
@Autowired(required = false)
@Getter
private RuleEngineDeviceStateManager deviceStateManager;
@Autowired @Autowired
@Getter @Getter
private TbTenantProfileCache tenantProfileCache; private TbTenantProfileCache tenantProfileCache;
@ -556,6 +561,10 @@ public class ActorSystemContext {
@Getter @Getter
private boolean externalNodeForceAck; private boolean externalNodeForceAck;
@Value("${state.rule.node.deviceState.rateLimit:1:1,30:60,60:3600}")
@Getter
private String deviceStateNodeRateLimitConfig;
@Getter @Getter
@Setter @Setter
private TbActorSystem actorSystem; private TbActorSystem actorSystem;

14
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -66,7 +66,6 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNoti
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.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg; import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry; import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry;
@ -90,7 +89,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.gen.transport.TransportProtos.UplinkNotificationMsg;
import org.thingsboard.server.service.rpc.RpcSubmitStrategy; import org.thingsboard.server.service.rpc.RpcSubmitStrategy;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -173,7 +174,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private EdgeId findRelatedEdgeId() { private EdgeId findRelatedEdgeId() {
List<EntityRelation> result = List<EntityRelation> result =
systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON); systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE);
if (result != null && result.size() > 0) { if (result != null && result.size() > 0) {
EntityRelation relationToEdge = result.get(0); EntityRelation relationToEdge = result.get(0);
if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) {
@ -212,8 +213,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (systemContext.isEdgesEnabled() && edgeId != null) { if (systemContext.isEdgesEnabled() && edgeId != null) {
log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId); log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId);
try { try {
saveRpcRequestToEdgeQueue(request, requestId).get(); if (systemContext.getEdgeService().isEdgeActiveAsync(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE).get()) {
sent = true; saveRpcRequestToEdgeQueue(request, requestId).get();
} else {
log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}. The Edge is currently offline or unreachable", tenantId, deviceId, edgeId.getId(), request);
}
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e); log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e);
} }
@ -470,7 +474,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
callback.onSuccess(); callback.onSuccess();
} }
private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) { private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, UplinkNotificationMsg uplinkNotificationMsg) {
String nodeId = sessionInfo.getNodeId(); String nodeId = sessionInfo.getNodeId();
sessions.entrySet().stream() sessions.entrySet().stream()
.filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC())) .filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC()))

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

@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager;
import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
@ -108,6 +109,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider; import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider;
import org.thingsboard.server.queue.common.SimpleTbQueueCallback;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
@ -213,7 +215,19 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) { if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain"); mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain");
} }
mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(onSuccess, onFailure)); mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(
metadata -> {
if (onSuccess != null) {
onSuccess.run();
}
},
t -> {
if (onFailure != null) {
onFailure.accept(t);
} else {
log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t);
}
}));
} }
@Override @Override
@ -299,7 +313,19 @@ class DefaultTbContext implements TbContext {
relationTypes.forEach(relationType -> relationTypes.forEach(relationType ->
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage)); mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage));
} }
mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(onSuccess, onFailure)); mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(
metadata -> {
if (onSuccess != null) {
onSuccess.run();
}
},
t -> {
if (onFailure != null) {
onFailure.accept(t);
} else {
log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t);
}
}));
} }
@Override @Override
@ -658,6 +684,16 @@ class DefaultTbContext implements TbContext {
return mainCtx.getDeviceCredentialsService(); return mainCtx.getDeviceCredentialsService();
} }
@Override
public RuleEngineDeviceStateManager getDeviceStateManager() {
return mainCtx.getDeviceStateManager();
}
@Override
public String getDeviceStateNodeRateLimitConfig() {
return mainCtx.getDeviceStateNodeRateLimitConfig();
}
@Override @Override
public TbClusterService getClusterService() { public TbClusterService getClusterService() {
return mainCtx.getClusterService(); return mainCtx.getClusterService();
@ -952,29 +988,4 @@ class DefaultTbContext implements TbContext {
return failureMessage; return failureMessage;
} }
private class SimpleTbQueueCallback implements TbQueueCallback {
private final Runnable onSuccess;
private final Consumer<Throwable> onFailure;
public SimpleTbQueueCallback(Runnable onSuccess, Consumer<Throwable> onFailure) {
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
if (onSuccess != null) {
onSuccess.run();
}
}
@Override
public void onFailure(Throwable t) {
if (onFailure != null) {
onFailure.accept(t);
} else {
log.debug("[{}] Failed to put item into queue", nodeCtx.getTenantId(), t);
}
}
}
} }

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

@ -16,7 +16,6 @@
package org.thingsboard.server.actors.ruleChain; package org.thingsboard.server.actors.ruleChain;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
@ -29,6 +28,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;

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

@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException; import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;

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

@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.security.event.UserSessionInvalidation
import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ActivateUserRequest;

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

@ -43,7 +43,7 @@ import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;

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

@ -126,6 +126,7 @@ public class ThingsboardInstallService {
case "3.6.2": case "3.6.2":
log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2");
systemDataLoaderService.updateDefaultNotificationConfigs();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break; break;
default: default:

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
@ -149,6 +150,9 @@ public class EdgeContextComponent {
@Autowired @Autowired
private ResourceService resourceService; private ResourceService resourceService;
@Autowired
private NotificationRuleProcessor notificationRuleProcessor;
@Autowired @Autowired
private AlarmEdgeProcessor alarmProcessor; private AlarmEdgeProcessor alarmProcessor;

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

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -263,7 +264,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) {
TenantId tenantId = edgeGrpcSession.getEdge().getTenantId(); Edge edge = edgeGrpcSession.getEdge();
TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
sessions.put(edgeId, edgeGrpcSession); sessions.put(edgeId, edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
@ -276,7 +278,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true);
long lastConnectTs = System.currentTimeMillis(); long lastConnectTs = System.currentTimeMillis();
save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs);
pushRuleEngineMessage(tenantId, edgeId, lastConnectTs, TbMsgType.CONNECT_EVENT); pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId); cancelScheduleEdgeEventsCheck(edgeId);
scheduleEdgeEventsCheck(edgeGrpcSession); scheduleEdgeEventsCheck(edgeGrpcSession);
} }
@ -381,7 +383,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void onEdgeDisconnect(EdgeId edgeId, UUID sessionId) { private void onEdgeDisconnect(Edge edge, UUID sessionId) {
EdgeId edgeId = edge.getId();
log.info("[{}][{}] edge disconnected!", edgeId, sessionId); log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId); EdgeGrpcSession toRemove = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) { if (toRemove.getSessionId().equals(sessionId)) {
@ -397,7 +400,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis(); long lastDisconnectTs = System.currentTimeMillis();
save(tenantId, edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); save(tenantId, edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId); cancelScheduleEdgeEventsCheck(edgeId);
} else { } else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
@ -452,25 +455,36 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, TbMsgType msgType) { private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) {
try { try {
EdgeId edgeId = edge.getId();
ObjectNode edgeState = JacksonUtil.newObjectNode(); ObjectNode edgeState = JacksonUtil.newObjectNode();
if (msgType.equals(TbMsgType.CONNECT_EVENT)) { boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType);
if (isConnected) {
edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true);
edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts);
} else { } else {
edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false); edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false);
edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts); edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts);
} }
ctx.getNotificationRuleProcessor().process(EdgeConnectionTrigger.builder()
.tenantId(tenantId)
.customerId(edge.getCustomerId())
.edgeId(edgeId)
.edgeName(edge.getName())
.connected(isConnected).build());
String data = JacksonUtil.toString(edgeState); String data = JacksonUtil.toString(edgeState);
TbMsgMetaData md = new TbMsgMetaData(); TbMsgMetaData md = new TbMsgMetaData();
if (!persistToTelemetry) { if (!persistToTelemetry) {
md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE);
md.putValue("edgeName", edge.getName());
md.putValue("edgeType", edge.getType());
} }
TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data);
clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Failed to push {}", tenantId, edgeId, msgType, e); log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e);
} }
} }
} }

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger;
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.page.SortOrder; import org.thingsboard.server.common.data.page.SortOrder;
@ -111,7 +112,7 @@ public final class EdgeGrpcSession implements Closeable {
private final UUID sessionId; private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener; private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
private final BiConsumer<EdgeId, UUID> sessionCloseListener; private final BiConsumer<Edge, UUID> sessionCloseListener;
private final EdgeSessionState sessionState = new EdgeSessionState(); private final EdgeSessionState sessionState = new EdgeSessionState();
@ -137,7 +138,7 @@ public final class EdgeGrpcSession implements Closeable {
private ScheduledExecutorService sendDownlinkExecutorService; private ScheduledExecutorService sendDownlinkExecutorService;
EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver<ResponseMsg> outputStream, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener, EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver<ResponseMsg> outputStream, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener,
BiConsumer<EdgeId, UUID> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { BiConsumer<Edge, UUID> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
this.sessionId = UUID.randomUUID(); this.sessionId = UUID.randomUUID();
this.ctx = ctx; this.ctx = ctx;
this.outputStream = outputStream; this.outputStream = outputStream;
@ -206,7 +207,7 @@ public final class EdgeGrpcSession implements Closeable {
connected = false; connected = false;
if (edge != null) { if (edge != null) {
try { try {
sessionCloseListener.accept(edge.getId(), sessionId); sessionCloseListener.accept(edge, sessionId);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
@ -314,7 +315,7 @@ public final class EdgeGrpcSession implements Closeable {
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Failed to send downlink message [{}]", this.tenantId, this.sessionId, downlinkMsg, e); log.error("[{}][{}] Failed to send downlink message [{}]", this.tenantId, this.sessionId, downlinkMsg, e);
connected = false; connected = false;
sessionCloseListener.accept(edge.getId(), sessionId); sessionCloseListener.accept(edge, sessionId);
} finally { } finally {
downlinkMsgLock.unlock(); downlinkMsgLock.unlock();
} }
@ -466,15 +467,26 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) { if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) {
List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values());
if (attempt > 1) { if (attempt > 1) {
log.warn("[{}][{}] Failed to deliver the batch: {}, attempt: {}", this.tenantId, this.sessionId, copy, attempt); String error = "Failed to deliver the batch";
String failureMsg = String.format("{%s}: {%s}", error, copy);
if (attempt == 2) {
// Send a failure notification only on the second attempt.
// This ensures that failure alerts are sent just once to avoid redundant notifications.
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
}
log.warn("[{}][{}] {}, attempt: {}", this.tenantId, this.sessionId, failureMsg, attempt);
} }
log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size()); log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size());
for (DownlinkMsg downlinkMsg : copy) { for (DownlinkMsg downlinkMsg : copy) {
if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) { if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) {
log.error("[{}][{}][{}] Downlink msg size [{}] exceeds client max inbound message size [{}]. Skipping this message. " + String error = String.format("Client max inbound message size [{%s}] is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " +
"Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it." + "env variable on the edge and restart it.", this.clientMaxInboundMessageSize);
"Message {}", this.tenantId, edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(), String message = String.format("Downlink msg size [{%s}] exceeds client max inbound message size [{%s}]. " +
this.clientMaxInboundMessageSize, downlinkMsg); "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), this.clientMaxInboundMessageSize);
log.error("[{}][{}][{}] {} Message {}", this.tenantId, edge.getId(), this.sessionId, message, downlinkMsg);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build());
sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId());
} else { } else {
sendDownlinkMsg(ResponseMsg.newBuilder() sendDownlinkMsg(ResponseMsg.newBuilder()
@ -485,8 +497,12 @@ public final class EdgeGrpcSession implements Closeable {
if (attempt < MAX_DOWNLINK_ATTEMPTS) { if (attempt < MAX_DOWNLINK_ATTEMPTS) {
scheduleDownlinkMsgsPackSend(attempt + 1); scheduleDownlinkMsgsPackSend(attempt + 1);
} else { } else {
String failureMsg = String.format("Failed to deliver messages: %s", copy);
log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
this.tenantId, this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); this.tenantId, this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg)
.error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build());
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} else { } else {
@ -791,7 +807,10 @@ public final class EdgeGrpcSession implements Closeable {
} }
} }
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.error("[{}][{}] Can't process uplink msg [{}]", this.tenantId, this.sessionId, uplinkMsg, e); log.error("[{}][{}] Can't process uplink msg [{}]", this.tenantId, this.sessionId, uplinkMsg, e);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
return Futures.allAsList(result); return Futures.allAsList(result);
@ -815,15 +834,22 @@ public final class EdgeGrpcSession implements Closeable {
.setMaxInboundMessageSize(maxInboundMessageSize) .setMaxInboundMessageSize(maxInboundMessageSize)
.build(); .build();
} }
String error = "Failed to validate the edge!";
String failureMsg = String.format("{%s} Provided request secret: %s", error, request.getEdgeSecret());
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
return ConnectResponseMsg.newBuilder() return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS)
.setErrorMsg("Failed to validate the edge!") .setErrorMsg(failureMsg)
.setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); .setConfiguration(EdgeConfiguration.getDefaultInstance()).build();
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to process edge connection!", request.getEdgeRoutingKey(), e); String failureMsg = "Failed to process edge connection!";
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
log.error(failureMsg, e);
return ConnectResponseMsg.newBuilder() return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE)
.setErrorMsg("Failed to process edge connection!") .setErrorMsg(failureMsg)
.setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); .setConfiguration(EdgeConfiguration.getDefaultInstance()).build();
} }
} }

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc.processor.rule;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
@ -53,6 +54,10 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText()); isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText());
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
if (!isRoot) {
Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeEvent.getEdgeId());
isRoot = edge.getRootRuleChainId().equals(ruleChainId);
}
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor)
ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))

115
application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java

@ -50,22 +50,15 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb
public Queue saveQueue(Queue queue) { public Queue saveQueue(Queue queue) {
boolean create = queue.getId() == null; boolean create = queue.getId() == null;
Queue oldQueue; Queue oldQueue;
if (create) { if (create) {
oldQueue = null; oldQueue = null;
} else { } else {
oldQueue = queueService.findQueueById(queue.getTenantId(), queue.getId()); oldQueue = queueService.findQueueById(queue.getTenantId(), queue.getId());
} }
//TODO: add checkNotNull
Queue savedQueue = queueService.saveQueue(queue); Queue savedQueue = queueService.saveQueue(queue);
createTopicsIfNeeded(savedQueue, oldQueue);
if (create) { tbClusterService.onQueuesUpdate(List.of(savedQueue));
onQueueCreated(savedQueue);
} else {
onQueueUpdated(savedQueue, oldQueue);
}
return savedQueue; return savedQueue;
} }
@ -73,54 +66,14 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb
public void deleteQueue(TenantId tenantId, QueueId queueId) { public void deleteQueue(TenantId tenantId, QueueId queueId) {
Queue queue = queueService.findQueueById(tenantId, queueId); Queue queue = queueService.findQueueById(tenantId, queueId);
queueService.deleteQueue(tenantId, queueId); queueService.deleteQueue(tenantId, queueId);
onQueueDeleted(queue); tbClusterService.onQueuesDelete(List.of(queue));
} }
@Override @Override
public void deleteQueueByQueueName(TenantId tenantId, String queueName) { public void deleteQueueByQueueName(TenantId tenantId, String queueName) {
Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, queueName); Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, queueName);
queueService.deleteQueue(tenantId, queue.getId()); queueService.deleteQueue(tenantId, queue.getId());
onQueueDeleted(queue); tbClusterService.onQueuesDelete(List.of(queue));
}
private void onQueueCreated(Queue queue) {
for (int i = 0; i < queue.getPartitions(); i++) {
tbQueueAdmin.createTopicIfNotExists(
new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(),
queue.getCustomProperties()
);
}
tbClusterService.onQueueChange(queue);
}
private void onQueueUpdated(Queue queue, Queue oldQueue) {
int oldPartitions = oldQueue.getPartitions();
int currentPartitions = queue.getPartitions();
if (currentPartitions != oldPartitions) {
if (currentPartitions > oldPartitions) {
log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName());
for (int i = oldPartitions; i < currentPartitions; i++) {
tbQueueAdmin.createTopicIfNotExists(
new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(),
queue.getCustomProperties()
);
}
tbClusterService.onQueueChange(queue);
} else {
log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName());
tbClusterService.onQueueChange(queue);
// TODO: move all the messages left in old partitions and delete topics
}
} else if (!oldQueue.equals(queue)) {
tbClusterService.onQueueChange(queue);
}
}
private void onQueueDeleted(Queue queue) {
tbClusterService.onQueueDelete(queue);
// queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId);
} }
@Override @Override
@ -176,26 +129,56 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb
log.debug("[{}] Handling profile queue config update: creating queues {}, updating {}, deleting {}. Affected tenants: {}", log.debug("[{}] Handling profile queue config update: creating queues {}, updating {}, deleting {}. Affected tenants: {}",
newTenantProfile.getUuidId(), toCreate, toUpdate, toRemove, tenantIds); newTenantProfile.getUuidId(), toCreate, toUpdate, toRemove, tenantIds);
} }
tenantIds.forEach(tenantId -> {
toCreate.forEach(key -> saveQueue(new Queue(tenantId, newQueues.get(key))));
toUpdate.forEach(key -> { List<Queue> updated = new ArrayList<>();
Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); List<Queue> deleted = new ArrayList<>();
Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); for (TenantId tenantId : tenantIds) {
queueToUpdate.setId(foundQueue.getId()); for (String name : toCreate) {
queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); updated.add(new Queue(tenantId, newQueues.get(name)));
}
if (!queueToUpdate.equals(foundQueue)) { for (String name : toUpdate) {
saveQueue(queueToUpdate); Queue queue = new Queue(tenantId, newQueues.get(name));
Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, name);
if (foundQueue != null) {
queue.setId(foundQueue.getId());
queue.setCreatedTime(foundQueue.getCreatedTime());
} }
}); if (!queue.equals(foundQueue)) {
updated.add(queue);
createTopicsIfNeeded(queue, foundQueue);
}
}
for (String name : toRemove) {
Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, name);
deleted.add(queue);
}
}
toRemove.forEach(q -> { if (!updated.isEmpty()) {
Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, q); updated = updated.stream()
QueueId queueIdForRemove = queue.getId(); .map(queueService::saveQueue)
deleteQueue(tenantId, queueIdForRemove); .collect(Collectors.toList());
tbClusterService.onQueuesUpdate(updated);
}
if (!deleted.isEmpty()) {
deleted.forEach(queue -> {
queueService.deleteQueue(queue.getTenantId(), queue.getId());
}); });
}); tbClusterService.onQueuesDelete(deleted);
}
}
private void createTopicsIfNeeded(Queue queue, Queue oldQueue) {
int newPartitions = queue.getPartitions();
int oldPartitions = oldQueue != null ? oldQueue.getPartitions() : 0;
for (int i = oldPartitions; i < newPartitions; i++) {
tbQueueAdmin.createTopicIfNotExists(
new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(),
queue.getCustomProperties()
);
}
} }
} }

16
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -691,7 +691,23 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
} }
@Override @Override
@SneakyThrows
public void updateDefaultNotificationConfigs() { public void updateDefaultNotificationConfigs() {
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500);
ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4));
log.info("Updating default edge failure notification configs for all tenants");
AtomicInteger count = new AtomicInteger();
for (TenantId tenantId : tenants) {
executor.submit(() -> {
notificationSettingsService.updateDefaultNotificationConfigs(tenantId);
int n = count.incrementAndGet();
if (n % 500 == 0) {
log.info("{} tenants processed", n);
}
});
}
executor.shutdown();
executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID); notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
} }

2
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -62,7 +62,7 @@ import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
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.TopicService; import org.thingsboard.server.queue.discovery.TopicService;

2
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -41,7 +41,7 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.notification.NotificationDeduplicationService; import org.thingsboard.server.queue.notification.NotificationDeduplicationService;
import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.executors.NotificationExecutorService;

62
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java

@ -0,0 +1,62 @@
/**
* 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.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EdgeCommunicationFailureNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Service
@RequiredArgsConstructor
public class EdgeCommunicationFailureTriggerProcessor implements NotificationRuleTriggerProcessor<EdgeCommunicationFailureTrigger, EdgeCommunicationFailureNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(EdgeCommunicationFailureTrigger trigger, EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig) {
if (CollectionUtils.isNotEmpty(triggerConfig.getEdges())) {
return !triggerConfig.getEdges().contains(trigger.getEdgeId().getId());
}
return true;
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(EdgeCommunicationFailureTrigger trigger) {
return EdgeCommunicationFailureNotificationInfo.builder()
.tenantId(trigger.getTenantId())
.edgeId(trigger.getEdgeId())
.customerId(trigger.getCustomerId())
.edgeName(trigger.getEdgeName())
.failureMsg(truncateFailureMsg(trigger.getFailureMsg()))
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
private String truncateFailureMsg(String input) {
int maxLength = 500;
if (input != null && input.length() > maxLength) {
return input.substring(0, maxLength);
}
return input;
}
}

60
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java

@ -0,0 +1,60 @@
/**
* 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.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EdgeConnectionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Service
@RequiredArgsConstructor
public class EdgeConnectionTriggerProcessor implements NotificationRuleTriggerProcessor<EdgeConnectionTrigger, EdgeConnectionNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(EdgeConnectionTrigger trigger, EdgeConnectionNotificationRuleTriggerConfig triggerConfig) {
EdgeConnectivityEvent event = trigger.isConnected() ? EdgeConnectivityEvent.CONNECTED : EdgeConnectivityEvent.DISCONNECTED;
if (CollectionUtils.isEmpty(triggerConfig.getNotifyOn()) || !triggerConfig.getNotifyOn().contains(event)) {
return false;
}
if (CollectionUtils.isNotEmpty(triggerConfig.getEdges())) {
return triggerConfig.getEdges().contains(trigger.getEdgeId().getId());
}
return true;
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(EdgeConnectionTrigger trigger) {
return EdgeConnectionNotificationInfo.builder()
.eventType(trigger.isConnected() ? "connected" : "disconnected")
.tenantId(trigger.getTenantId())
.customerId(trigger.getCustomerId())
.edgeId(trigger.getEdgeId())
.edgeName(trigger.getEdgeName())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
}

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

@ -62,6 +62,8 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.dao.edge.EdgeService; 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.QueueUpdateMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
@ -81,9 +83,11 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Optional; import java.util.Optional;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.util.ProtoUtils.toProto; import static org.thingsboard.server.common.util.ProtoUtils.toProto;
@ -563,40 +567,40 @@ public class DefaultTbClusterService implements TbClusterService {
} }
@Override @Override
public void onQueueChange(Queue queue) { public void onQueuesUpdate(List<Queue> queues) {
log.trace("[{}][{}] Processing queue change [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); List<QueueUpdateMsg> queueUpdateMsgs = queues.stream()
.map(queue -> QueueUpdateMsg.newBuilder()
TransportProtos.QueueUpdateMsg queueUpdateMsg = TransportProtos.QueueUpdateMsg.newBuilder() .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits())
.setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits())
.setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) .setQueueIdMSB(queue.getId().getId().getMostSignificantBits())
.setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits())
.setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) .setQueueName(queue.getName())
.setQueueName(queue.getName()) .setQueueTopic(queue.getTopic())
.setQueueTopic(queue.getTopic()) .setPartitions(queue.getPartitions())
.setPartitions(queue.getPartitions()) .build())
.build(); .collect(Collectors.toList());
ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().addAllQueueUpdateMsgs(queueUpdateMsgs).build();
ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().addAllQueueUpdateMsgs(queueUpdateMsgs).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().addAllQueueUpdateMsgs(queueUpdateMsgs).build();
doSendQueueNotifications(ruleEngineMsg, coreMsg, transportMsg); doSendQueueNotifications(ruleEngineMsg, coreMsg, transportMsg);
} }
@Override @Override
public void onQueueDelete(Queue queue) { public void onQueuesDelete(List<Queue> queues) {
log.trace("[{}][{}] Processing queue delete [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); List<QueueDeleteMsg> queueDeleteMsgs = queues.stream()
.map(queue -> QueueDeleteMsg.newBuilder()
TransportProtos.QueueDeleteMsg queueDeleteMsg = TransportProtos.QueueDeleteMsg.newBuilder() .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits())
.setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits())
.setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) .setQueueIdMSB(queue.getId().getId().getMostSignificantBits())
.setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits())
.setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) .setQueueName(queue.getName())
.setQueueName(queue.getName()) .build())
.build(); .collect(Collectors.toList());
ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().addAllQueueDeleteMsgs(queueDeleteMsgs).build();
ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().addAllQueueDeleteMsgs(queueDeleteMsgs).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().addAllQueueDeleteMsgs(queueDeleteMsgs).build();
doSendQueueNotifications(ruleEngineMsg, coreMsg, transportMsg); doSendQueueNotifications(ruleEngineMsg, coreMsg, transportMsg);
} }

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

@ -15,6 +15,9 @@
*/ */
package org.thingsboard.server.service.queue; package org.thingsboard.server.service.queue;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -148,6 +151,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
protected volatile ExecutorService consumersExecutor; protected volatile ExecutorService consumersExecutor;
protected volatile ExecutorService usageStatsExecutor; protected volatile ExecutorService usageStatsExecutor;
private volatile ExecutorService firmwareStatesExecutor; private volatile ExecutorService firmwareStatesExecutor;
private volatile ListeningExecutorService deviceActivityEventsExecutor;
public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory, public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory,
ActorSystemContext actorContext, ActorSystemContext actorContext,
@ -195,6 +199,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-core-consumer")); this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-core-consumer"));
this.usageStatsExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-usage-stats-consumer")); this.usageStatsExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-usage-stats-consumer"));
this.firmwareStatesExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-firmware-notifications-consumer")); this.firmwareStatesExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-firmware-notifications-consumer"));
this.deviceActivityEventsExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-device-activity-events-executor")));
} }
@PreDestroy @PreDestroy
@ -209,6 +214,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
if (firmwareStatesExecutor != null) { if (firmwareStatesExecutor != null) {
firmwareStatesExecutor.shutdownNow(); firmwareStatesExecutor.shutdownNow();
} }
if (deviceActivityEventsExecutor != null) {
deviceActivityEventsExecutor.shutdownNow();
}
} }
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
@ -265,14 +273,23 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg()); log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback); forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
} else if (toCoreMsg.hasDeviceStateServiceMsg()) { } else if (toCoreMsg.hasDeviceStateServiceMsg()) {
log.trace("[{}] Forwarding message to state service {}", id, toCoreMsg.getDeviceStateServiceMsg()); log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback); forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
} else if (toCoreMsg.hasEdgeNotificationMsg()) { } else if (toCoreMsg.hasEdgeNotificationMsg()) {
log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg()); log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback); forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
} else if (toCoreMsg.hasDeviceConnectMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg());
forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback);
} else if (toCoreMsg.hasDeviceActivityMsg()) { } else if (toCoreMsg.hasDeviceActivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg()); log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg());
forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback); forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback);
} else if (toCoreMsg.hasDeviceDisconnectMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg());
forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback);
} else if (toCoreMsg.hasDeviceInactivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg());
forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorNotification()) { } else if (toCoreMsg.hasToDeviceActorNotification()) {
TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification()); TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification());
if (actorMsg != null) { if (actorMsg != null) {
@ -391,13 +408,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} else if (!toCoreNotification.getFromEdgeSyncResponseMsg().isEmpty()) { } else if (!toCoreNotification.getFromEdgeSyncResponseMsg().isEmpty()) {
//will be removed in 3.6.1 in favour of hasFromEdgeSyncResponse() //will be removed in 3.6.1 in favour of hasFromEdgeSyncResponse()
forwardToAppActor(id, encodingService.decode(toCoreNotification.getFromEdgeSyncResponseMsg().toByteArray()), callback); forwardToAppActor(id, encodingService.decode(toCoreNotification.getFromEdgeSyncResponseMsg().toByteArray()), callback);
} else if (toCoreNotification.hasQueueUpdateMsg()) { } else if (toCoreNotification.getQueueUpdateMsgsCount() > 0) {
TransportProtos.QueueUpdateMsg queue = toCoreNotification.getQueueUpdateMsg(); partitionService.updateQueues(toCoreNotification.getQueueUpdateMsgsList());
partitionService.updateQueue(queue);
callback.onSuccess(); callback.onSuccess();
} else if (toCoreNotification.hasQueueDeleteMsg()) { } else if (toCoreNotification.getQueueDeleteMsgsCount() > 0) {
TransportProtos.QueueDeleteMsg queue = toCoreNotification.getQueueDeleteMsg(); partitionService.removeQueues(toCoreNotification.getQueueDeleteMsgsList());
partitionService.removeQueue(queue);
callback.onSuccess(); callback.onSuccess();
} else if (toCoreNotification.hasVcResponseMsg()) { } else if (toCoreNotification.hasVcResponseMsg()) {
vcQueueService.processResponse(toCoreNotification.getVcResponseMsg()); vcQueueService.processResponse(toCoreNotification.getVcResponseMsg());
@ -643,25 +658,71 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} }
} }
private void forwardToStateService(DeviceStateServiceMsgProto deviceStateServiceMsg, TbCallback callback) { void forwardToStateService(DeviceStateServiceMsgProto deviceStateServiceMsg, TbCallback callback) {
if (statsEnabled) { if (statsEnabled) {
stats.log(deviceStateServiceMsg); stats.log(deviceStateServiceMsg);
} }
stateService.onQueueMsg(deviceStateServiceMsg, callback); stateService.onQueueMsg(deviceStateServiceMsg, callback);
} }
private void forwardToStateService(TransportProtos.DeviceActivityProto deviceActivityMsg, TbCallback callback) { void forwardToStateService(TransportProtos.DeviceConnectProto deviceConnectMsg, TbCallback callback) {
if (statsEnabled) {
stats.log(deviceConnectMsg);
}
var tenantId = toTenantId(deviceConnectMsg.getTenantIdMSB(), deviceConnectMsg.getTenantIdLSB());
var deviceId = new DeviceId(new UUID(deviceConnectMsg.getDeviceIdMSB(), deviceConnectMsg.getDeviceIdLSB()));
ListenableFuture<?> future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceConnect(tenantId, deviceId, deviceConnectMsg.getLastConnectTime()));
DonAsynchron.withCallback(future,
__ -> callback.onSuccess(),
t -> {
log.warn("[{}] Failed to process device connect message for device [{}]", tenantId.getId(), deviceId.getId(), t);
callback.onFailure(t);
});
}
void forwardToStateService(TransportProtos.DeviceActivityProto deviceActivityMsg, TbCallback callback) {
if (statsEnabled) { if (statsEnabled) {
stats.log(deviceActivityMsg); stats.log(deviceActivityMsg);
} }
TenantId tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB()); var tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB());
DeviceId deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB())); var deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB()));
try { ListenableFuture<?> future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime()));
stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime()); DonAsynchron.withCallback(future,
callback.onSuccess(); __ -> callback.onSuccess(),
} catch (Exception e) { t -> {
callback.onFailure(new RuntimeException("Failed update device activity for device [" + deviceId.getId() + "]!", e)); log.warn("[{}] Failed to process device activity message for device [{}]", tenantId.getId(), deviceId.getId(), t);
callback.onFailure(new RuntimeException("Failed to update device activity for device [" + deviceId.getId() + "]!", t));
});
}
void forwardToStateService(TransportProtos.DeviceDisconnectProto deviceDisconnectMsg, TbCallback callback) {
if (statsEnabled) {
stats.log(deviceDisconnectMsg);
}
var tenantId = toTenantId(deviceDisconnectMsg.getTenantIdMSB(), deviceDisconnectMsg.getTenantIdLSB());
var deviceId = new DeviceId(new UUID(deviceDisconnectMsg.getDeviceIdMSB(), deviceDisconnectMsg.getDeviceIdLSB()));
ListenableFuture<?> future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceDisconnect(tenantId, deviceId, deviceDisconnectMsg.getLastDisconnectTime()));
DonAsynchron.withCallback(future,
__ -> callback.onSuccess(),
t -> {
log.warn("[{}] Failed to process device disconnect message for device [{}]", tenantId.getId(), deviceId.getId(), t);
callback.onFailure(t);
});
}
void forwardToStateService(TransportProtos.DeviceInactivityProto deviceInactivityMsg, TbCallback callback) {
if (statsEnabled) {
stats.log(deviceInactivityMsg);
} }
var tenantId = toTenantId(deviceInactivityMsg.getTenantIdMSB(), deviceInactivityMsg.getTenantIdLSB());
var deviceId = new DeviceId(new UUID(deviceInactivityMsg.getDeviceIdMSB(), deviceInactivityMsg.getDeviceIdLSB()));
ListenableFuture<?> future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceInactivity(tenantId, deviceId, deviceInactivityMsg.getLastInactivityTime()));
DonAsynchron.withCallback(future,
__ -> callback.onSuccess(),
t -> {
log.warn("[{}] Failed to process device inactivity message for device [{}]", tenantId.getId(), deviceId.getId(), t);
callback.onFailure(t);
});
} }
private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) { private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {

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

@ -36,6 +36,8 @@ import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg;
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.PartitionService;
@ -164,11 +166,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
, proto.getResponse(), error); , proto.getResponse(), error);
tbDeviceRpcService.processRpcResponseFromDevice(response); tbDeviceRpcService.processRpcResponseFromDevice(response);
callback.onSuccess(); callback.onSuccess();
} else if (nfMsg.hasQueueUpdateMsg()) { } else if (nfMsg.getQueueUpdateMsgsCount() > 0) {
updateQueue(nfMsg.getQueueUpdateMsg()); updateQueues(nfMsg.getQueueUpdateMsgsList());
callback.onSuccess(); callback.onSuccess();
} else if (nfMsg.hasQueueDeleteMsg()) { } else if (nfMsg.getQueueDeleteMsgsCount() > 0) {
deleteQueue(nfMsg.getQueueDeleteMsg()); deleteQueues(nfMsg.getQueueDeleteMsgsList());
callback.onSuccess(); callback.onSuccess();
} else { } else {
log.trace("Received notification with missing handler"); log.trace("Received notification with missing handler");
@ -176,39 +178,48 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
} }
} }
private void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { private void updateQueues(List<QueueUpdateMsg> queueUpdateMsgs) {
log.info("Received queue update msg: [{}]", queueUpdateMsg); boolean partitionsChanged = false;
TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); for (QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) {
if (partitionService.isManagedByCurrentService(tenantId)) { log.info("Received queue update msg: [{}]", queueUpdateMsg);
QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB()));
String queueName = queueUpdateMsg.getQueueName(); if (partitionService.isManagedByCurrentService(tenantId)) {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId); QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB()));
Queue queue = queueService.findQueueById(tenantId, queueId); String queueName = queueUpdateMsg.getQueueName();
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId);
TbRuleEngineQueueConsumerManager consumerManager = getOrCreateConsumer(queueKey); Queue queue = queueService.findQueueById(tenantId, queueId);
Queue oldQueue = consumerManager.getQueue();
consumerManager.update(queue); TbRuleEngineQueueConsumerManager consumerManager = getOrCreateConsumer(queueKey);
Queue oldQueue = consumerManager.getQueue();
if (oldQueue != null && queue.getPartitions() == oldQueue.getPartitions()) { consumerManager.update(queue);
return;
if (oldQueue == null || queue.getPartitions() != oldQueue.getPartitions()) {
partitionsChanged = true;
}
} else {
partitionsChanged = true;
} }
} }
partitionService.updateQueue(queueUpdateMsg); if (partitionsChanged) {
partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), partitionService.updateQueues(queueUpdateMsgs);
new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE))); partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(),
new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE)));
}
} }
private void deleteQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { private void deleteQueues(List<QueueDeleteMsg> queueDeleteMsgs) {
log.info("Received queue delete msg: [{}]", queueDeleteMsg); for (QueueDeleteMsg queueDeleteMsg : queueDeleteMsgs) {
TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); log.info("Received queue delete msg: [{}]", queueDeleteMsg);
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB()));
var consumerManager = consumers.remove(queueKey); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId);
if (consumerManager != null) { var consumerManager = consumers.remove(queueKey);
consumerManager.delete(true); if (consumerManager != null) {
consumerManager.delete(true);
}
} }
partitionService.removeQueue(queueDeleteMsg); partitionService.removeQueues(queueDeleteMsgs);
partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE))); partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE)));
} }

28
application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java

@ -37,7 +37,10 @@ public class TbCoreConsumerStats {
public static final String DEVICE_STATES = "deviceState"; public static final String DEVICE_STATES = "deviceState";
public static final String SUBSCRIPTION_MSGS = "subMsgs"; public static final String SUBSCRIPTION_MSGS = "subMsgs";
public static final String EDGE_NOTIFICATIONS = "edgeNfs"; public static final String EDGE_NOTIFICATIONS = "edgeNfs";
public static final String DEVICE_CONNECTS = "deviceConnect";
public static final String DEVICE_ACTIVITIES = "deviceActivity"; public static final String DEVICE_ACTIVITIES = "deviceActivity";
public static final String DEVICE_DISCONNECTS = "deviceDisconnect";
public static final String DEVICE_INACTIVITIES = "deviceInactivity";
public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine
public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl"; public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl";
@ -63,7 +66,10 @@ public class TbCoreConsumerStats {
private final StatsCounter deviceStateCounter; private final StatsCounter deviceStateCounter;
private final StatsCounter subscriptionMsgCounter; private final StatsCounter subscriptionMsgCounter;
private final StatsCounter edgeNotificationsCounter; private final StatsCounter edgeNotificationsCounter;
private final StatsCounter deviceConnectsCounter;
private final StatsCounter deviceActivitiesCounter; private final StatsCounter deviceActivitiesCounter;
private final StatsCounter deviceDisconnectsCounter;
private final StatsCounter deviceInactivitiesCounter;
private final StatsCounter toCoreNfOtherCounter; private final StatsCounter toCoreNfOtherCounter;
private final StatsCounter toCoreNfComponentLifecycleCounter; private final StatsCounter toCoreNfComponentLifecycleCounter;
@ -94,7 +100,10 @@ public class TbCoreConsumerStats {
this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES));
this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS));
this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS));
this.deviceConnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CONNECTS));
this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES)); this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES));
this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS));
this.deviceInactivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITIES));
// Core notification counters // Core notification counters
this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER)); this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER));
@ -152,11 +161,26 @@ public class TbCoreConsumerStats {
edgeNotificationsCounter.increment(); edgeNotificationsCounter.increment();
} }
public void log(TransportProtos.DeviceConnectProto msg) {
totalCounter.increment();
deviceConnectsCounter.increment();
}
public void log(TransportProtos.DeviceActivityProto msg) { public void log(TransportProtos.DeviceActivityProto msg) {
totalCounter.increment(); totalCounter.increment();
deviceActivitiesCounter.increment(); deviceActivitiesCounter.increment();
} }
public void log(TransportProtos.DeviceDisconnectProto msg) {
totalCounter.increment();
deviceDisconnectsCounter.increment();
}
public void log(TransportProtos.DeviceInactivityProto msg) {
totalCounter.increment();
deviceInactivitiesCounter.increment();
}
public void log(TransportProtos.SubscriptionMgrMsgProto msg) { public void log(TransportProtos.SubscriptionMgrMsgProto msg) {
totalCounter.increment(); totalCounter.increment();
subscriptionMsgCounter.increment(); subscriptionMsgCounter.increment();
@ -184,9 +208,9 @@ public class TbCoreConsumerStats {
toCoreNfEdgeSyncResponseCounter.increment(); toCoreNfEdgeSyncResponseCounter.increment();
} else if (!msg.getFromEdgeSyncResponseMsg().isEmpty()) { } else if (!msg.getFromEdgeSyncResponseMsg().isEmpty()) {
toCoreNfEdgeSyncResponseCounter.increment(); toCoreNfEdgeSyncResponseCounter.increment();
} else if (msg.hasQueueUpdateMsg()) { } else if (msg.getQueueUpdateMsgsCount() > 0) {
toCoreNfQueueUpdateCounter.increment(); toCoreNfQueueUpdateCounter.increment();
} else if (msg.hasQueueDeleteMsg()) { } else if (msg.getQueueDeleteMsgsCount() > 0) {
toCoreNfQueueDeleteCounter.increment(); toCoreNfQueueDeleteCounter.increment();
} else if (msg.hasVcResponseMsg()) { } else if (msg.hasVcResponseMsg()) {
toCoreNfVersionControlResponseCounter.increment(); toCoreNfVersionControlResponseCounter.increment();

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

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

12
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.queue.processing; package org.thingsboard.server.service.queue.processing;
import org.thingsboard.server.common.data.StringUtils;
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;
@ -51,7 +52,16 @@ public abstract class AbstractTbRuleEngineSubmitStrategy implements TbRuleEngine
List<IdMsgPair<TransportProtos.ToRuleEngineMsg>> newOrderedMsgList = new ArrayList<>(reprocessMap.size()); List<IdMsgPair<TransportProtos.ToRuleEngineMsg>> newOrderedMsgList = new ArrayList<>(reprocessMap.size());
for (IdMsgPair<TransportProtos.ToRuleEngineMsg> pair : orderedMsgList) { for (IdMsgPair<TransportProtos.ToRuleEngineMsg> pair : orderedMsgList) {
if (reprocessMap.containsKey(pair.uuid)) { if (reprocessMap.containsKey(pair.uuid)) {
newOrderedMsgList.add(pair); if (StringUtils.isNotEmpty(pair.getMsg().getValue().getFailureMessage())) {
var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.getMsg().getValue())
.clearFailureMessage()
.clearRelationTypes()
.build();
var newMsg = new TbProtoQueueMsg<>(pair.getMsg().getKey(), toRuleEngineMsg, pair.getMsg().getHeaders());
newOrderedMsgList.add(new IdMsgPair<>(pair.getUuid(), newMsg));
} else {
newOrderedMsgList.add(pair);
}
} }
} }
orderedMsgList = newOrderedMsgList; orderedMsgList = newOrderedMsgList;

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;

128
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
@ -221,25 +222,35 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
@Override @Override
public void onDeviceConnect(TenantId tenantId, DeviceId deviceId) { public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long lastConnectTime) {
if (cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId)) { if (cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId)) {
return;
}
if (lastConnectTime < 0) {
log.trace("[{}][{}] On device connect: received negative last connect ts [{}]. Skipping this event.",
tenantId.getId(), deviceId.getId(), lastConnectTime);
return; return;
} }
log.trace("on Device Connect [{}]", deviceId.getId());
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
long ts = getCurrentTimeMillis(); long currentLastConnectTime = stateData.getState().getLastConnectTime();
stateData.getState().setLastConnectTime(ts); if (lastConnectTime <= currentLastConnectTime) {
save(deviceId, LAST_CONNECT_TIME, ts); log.trace("[{}][{}] On device connect: received outdated last connect ts [{}]. Skipping this event. Current last connect ts [{}].",
tenantId.getId(), deviceId.getId(), lastConnectTime, currentLastConnectTime);
return;
}
log.trace("[{}][{}] On device connect: processing connect event with ts [{}].", tenantId.getId(), deviceId.getId(), lastConnectTime);
stateData.getState().setLastConnectTime(lastConnectTime);
save(deviceId, LAST_CONNECT_TIME, lastConnectTime);
pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT); pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT);
checkAndUpdateState(deviceId, stateData); checkAndUpdateState(deviceId, stateData);
} }
@Override @Override
public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) { public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) {
if (cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId)) { if (cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId)) {
return; return;
} }
log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity); log.trace("[{}] on Device Activity [{}], lastReportedActivity [{}]", tenantId.getId(), deviceId.getId(), lastReportedActivity);
final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
if (lastReportedActivity > 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) { if (lastReportedActivity > 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) {
updateActivityState(deviceId, stateData, lastReportedActivity); updateActivityState(deviceId, stateData, lastReportedActivity);
@ -261,37 +272,75 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
onDeviceActivityStatusChange(deviceId, true, stateData); onDeviceActivityStatusChange(deviceId, true, stateData);
} }
} else { } else {
log.debug("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); log.debug("updateActivityState - fetched state IS NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity);
cleanupEntity(deviceId); cleanupEntity(deviceId);
} }
} }
@Override @Override
public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) { public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long lastDisconnectTime) {
if (cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId)) { if (cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId)) {
return;
}
if (lastDisconnectTime < 0) {
log.trace("[{}][{}] On device disconnect: received negative last disconnect ts [{}]. Skipping this event.",
tenantId.getId(), deviceId.getId(), lastDisconnectTime);
return; return;
} }
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
long ts = getCurrentTimeMillis(); long currentLastDisconnectTime = stateData.getState().getLastDisconnectTime();
stateData.getState().setLastDisconnectTime(ts); if (lastDisconnectTime <= currentLastDisconnectTime) {
save(deviceId, LAST_DISCONNECT_TIME, ts); log.trace("[{}][{}] On device disconnect: received outdated last disconnect ts [{}]. Skipping this event. Current last disconnect ts [{}].",
tenantId.getId(), deviceId.getId(), lastDisconnectTime, currentLastDisconnectTime);
return;
}
log.trace("[{}][{}] On device disconnect: processing disconnect event with ts [{}].", tenantId.getId(), deviceId.getId(), lastDisconnectTime);
stateData.getState().setLastDisconnectTime(lastDisconnectTime);
save(deviceId, LAST_DISCONNECT_TIME, lastDisconnectTime);
pushRuleEngineMessage(stateData, TbMsgType.DISCONNECT_EVENT); pushRuleEngineMessage(stateData, TbMsgType.DISCONNECT_EVENT);
} }
@Override @Override
public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) { public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) {
if (cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId)) { if (cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId)) {
return; return;
} }
if (inactivityTimeout <= 0L) { if (inactivityTimeout <= 0L) {
inactivityTimeout = defaultInactivityTimeoutMs; inactivityTimeout = defaultInactivityTimeoutMs;
} }
log.trace("on Device Activity Timeout Update device id {} inactivityTimeout {}", deviceId, inactivityTimeout); log.trace("[{}] on Device Activity Timeout Update device id {} inactivityTimeout {}", tenantId.getId(), deviceId.getId(), inactivityTimeout);
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
stateData.getState().setInactivityTimeout(inactivityTimeout); stateData.getState().setInactivityTimeout(inactivityTimeout);
checkAndUpdateState(deviceId, stateData); checkAndUpdateState(deviceId, stateData);
} }
@Override
public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long lastInactivityTime) {
if (cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId)) {
return;
}
if (lastInactivityTime < 0) {
log.trace("[{}][{}] On device inactivity: received negative last inactivity ts [{}]. Skipping this event.",
tenantId.getId(), deviceId.getId(), lastInactivityTime);
return;
}
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
long currentLastInactivityAlarmTime = stateData.getState().getLastInactivityAlarmTime();
if (lastInactivityTime <= currentLastInactivityAlarmTime) {
log.trace("[{}][{}] On device inactivity: received last inactivity ts [{}] is less than current last inactivity ts [{}]. Skipping this event.",
tenantId.getId(), deviceId.getId(), lastInactivityTime, currentLastInactivityAlarmTime);
return;
}
long currentLastActivityTime = stateData.getState().getLastActivityTime();
if (lastInactivityTime <= currentLastActivityTime) {
log.trace("[{}][{}] On device inactivity: received last inactivity ts [{}] is less or equal to current last activity ts [{}]. Skipping this event.",
tenantId.getId(), deviceId.getId(), lastInactivityTime, currentLastActivityTime);
return;
}
log.trace("[{}][{}] On device inactivity: processing inactivity event with ts [{}].", tenantId.getId(), deviceId.getId(), lastInactivityTime);
reportInactivity(lastInactivityTime, deviceId, stateData);
}
@Override @Override
public void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback callback) { public void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback callback) {
try { try {
@ -497,10 +546,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
&& (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() <= state.getLastActivityTime()) && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() <= state.getLastActivityTime())
&& stateData.getDeviceCreationTime() + state.getInactivityTimeout() <= ts) { && stateData.getDeviceCreationTime() + state.getInactivityTimeout() <= ts) {
if (partitionService.resolve(ServiceType.TB_CORE, stateData.getTenantId(), deviceId).isMyPartition()) { if (partitionService.resolve(ServiceType.TB_CORE, stateData.getTenantId(), deviceId).isMyPartition()) {
state.setActive(false); reportInactivity(ts, deviceId, stateData);
state.setLastInactivityAlarmTime(ts);
onDeviceActivityStatusChange(deviceId, false, stateData);
save(deviceId, INACTIVITY_ALARM_TIME, ts);
} else { } else {
cleanupEntity(deviceId); cleanupEntity(deviceId);
} }
@ -511,32 +557,34 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
} }
private void reportInactivity(long ts, DeviceId deviceId, DeviceStateData stateData) {
DeviceState state = stateData.getState();
state.setActive(false);
state.setLastInactivityAlarmTime(ts);
save(deviceId, INACTIVITY_ALARM_TIME, ts);
onDeviceActivityStatusChange(deviceId, false, stateData);
}
boolean isActive(long ts, DeviceState state) { boolean isActive(long ts, DeviceState state) {
return ts < state.getLastActivityTime() + state.getInactivityTimeout(); return ts < state.getLastActivityTime() + state.getInactivityTimeout();
} }
@Nonnull @Nonnull
DeviceStateData getOrFetchDeviceStateData(DeviceId deviceId) { DeviceStateData getOrFetchDeviceStateData(DeviceId deviceId) {
DeviceStateData deviceStateData = deviceStates.get(deviceId); return deviceStates.computeIfAbsent(deviceId, this::fetchDeviceStateDataUsingSeparateRequests);
if (deviceStateData != null) {
return deviceStateData;
}
return fetchDeviceStateDataUsingEntityDataQuery(deviceId);
} }
DeviceStateData fetchDeviceStateDataUsingEntityDataQuery(final DeviceId deviceId) { DeviceStateData fetchDeviceStateDataUsingSeparateRequests(final DeviceId deviceId) {
final Device device = deviceService.findDeviceById(TenantId.SYS_TENANT_ID, deviceId); final Device device = deviceService.findDeviceById(TenantId.SYS_TENANT_ID, deviceId);
if (device == null) { if (device == null) {
log.warn("[{}] Failed to fetch device by Id!", deviceId); log.warn("[{}] Failed to fetch device by Id!", deviceId);
throw new RuntimeException("Failed to fetch device by Id " + deviceId); throw new RuntimeException("Failed to fetch device by id [" + deviceId + "]!");
} }
try { try {
DeviceStateData deviceStateData = fetchDeviceState(device).get(); return fetchDeviceState(device).get();
deviceStates.putIfAbsent(deviceId, deviceStateData);
return deviceStateData;
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
log.warn("[{}] Failed to fetch device state!", deviceId, e); log.warn("[{}] Failed to fetch device state!", deviceId, e);
throw new RuntimeException(e); throw new RuntimeException("Failed to fetch device state for device [" + deviceId + "]");
} }
} }
@ -553,7 +601,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.build()); .build());
} }
private boolean cleanDeviceStateIfBelongsExternalPartition(TenantId tenantId, final DeviceId deviceId) { boolean cleanDeviceStateIfBelongsToExternalPartition(TenantId tenantId, final DeviceId deviceId) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
boolean cleanup = !partitionedEntities.containsKey(tpi); boolean cleanup = !partitionedEntities.containsKey(tpi);
if (cleanup) { if (cleanup) {
@ -621,7 +669,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L); long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L); long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs); long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
//Actual active state by wall-clock will updated outside this method. This method is only for fetch persistent state // Actual active state by wall-clock will be updated outside this method. This method is only for fetching persistent state
final boolean active = getEntryValue(data, ACTIVITY_STATE, false); final boolean active = getEntryValue(data, ACTIVITY_STATE, false);
DeviceState deviceState = DeviceState.builder() DeviceState deviceState = DeviceState.builder()
.active(active) .active(active)
@ -646,7 +694,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return deviceStateData; return deviceStateData;
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to fetch device state data", device.getId(), e); log.warn("[{}] Failed to fetch device state data", device.getId(), e);
throw new RuntimeException(e); throw new RuntimeException("Failed to fetch device state data for device [" + device.getId() + "]", e);
} }
} }
}; };
@ -670,8 +718,13 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
return success ? result : result.stream().filter(Objects::nonNull).collect(Collectors.toList()); return success ? result : result.stream().filter(Objects::nonNull).collect(Collectors.toList());
} catch (InterruptedException | ExecutionException | TimeoutException e) { } catch (InterruptedException | ExecutionException | TimeoutException e) {
log.warn("Failed to initialized device state futures for ids: {} due to:", deviceIds, e); String deviceIdsStr = deviceIds.stream()
throw new RuntimeException(e); .map(DeviceIdInfo::getDeviceId)
.map(UUIDBased::getId)
.map(UUID::toString)
.collect(Collectors.joining(", "));
log.warn("Failed to initialized device state futures for ids [{}] due to:", deviceIdsStr, e);
throw new RuntimeException("Failed to initialized device state futures for ids [" + deviceIdsStr + "]!", e);
} }
} }
@ -701,7 +754,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
deviceIdInfo.getDeviceId(), inactivityTimeout); deviceIdInfo.getDeviceId(), inactivityTimeout);
inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs); inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
} }
//Actual active state by wall-clock will updated outside this method. This method is only for fetch persistent state // Actual active state by wall-clock will be updated outside this method. This method is only for fetching persistent state
final boolean active = getEntryValue(ed, getKeyType(), ACTIVITY_STATE, false); final boolean active = getEntryValue(ed, getKeyType(), ACTIVITY_STATE, false);
DeviceState deviceState = DeviceState.builder() DeviceState deviceState = DeviceState.builder()
.active(active) .active(active)
@ -757,7 +810,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return defaultValue; return defaultValue;
} }
private long getEntryValue(List<? extends KvEntry> kvEntries, String attributeName, long defaultValue) { private long getEntryValue(List<? extends KvEntry> kvEntries, String attributeName, long defaultValue) {
if (kvEntries != null) { if (kvEntries != null) {
for (KvEntry entry : kvEntries) { for (KvEntry entry : kvEntries) {

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

@ -0,0 +1,196 @@
/**
* 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.state;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.SimpleTbQueueCallback;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Service
@TbRuleEngineComponent
public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager {
private final TbServiceInfoProvider serviceInfoProvider;
private final PartitionService partitionService;
private final Optional<DeviceStateService> deviceStateService;
private final TbClusterService clusterService;
public DefaultRuleEngineDeviceStateManager(
TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService,
Optional<DeviceStateService> deviceStateServiceOptional, TbClusterService clusterService
) {
this.serviceInfoProvider = serviceInfoProvider;
this.partitionService = partitionService;
this.deviceStateService = deviceStateServiceOptional;
this.clusterService = clusterService;
}
@Getter
private abstract static class ConnectivityEventInfo {
private final TenantId tenantId;
private final DeviceId deviceId;
private final long eventTime;
private ConnectivityEventInfo(TenantId tenantId, DeviceId deviceId, long eventTime) {
this.tenantId = tenantId;
this.deviceId = deviceId;
this.eventTime = eventTime;
}
abstract void forwardToLocalService();
abstract TransportProtos.ToCoreMsg toQueueMsg();
}
@Override
public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) {
routeEvent(new ConnectivityEventInfo(tenantId, deviceId, connectTime) {
@Override
void forwardToLocalService() {
deviceStateService.ifPresent(service -> service.onDeviceConnect(tenantId, deviceId, connectTime));
}
@Override
TransportProtos.ToCoreMsg toQueueMsg() {
var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastConnectTime(connectTime)
.build();
return TransportProtos.ToCoreMsg.newBuilder()
.setDeviceConnectMsg(deviceConnectMsg)
.build();
}
}, callback);
}
@Override
public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) {
routeEvent(new ConnectivityEventInfo(tenantId, deviceId, activityTime) {
@Override
void forwardToLocalService() {
deviceStateService.ifPresent(service -> service.onDeviceActivity(tenantId, deviceId, activityTime));
}
@Override
TransportProtos.ToCoreMsg toQueueMsg() {
var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastActivityTime(activityTime)
.build();
return TransportProtos.ToCoreMsg.newBuilder()
.setDeviceActivityMsg(deviceActivityMsg)
.build();
}
}, callback);
}
@Override
public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) {
routeEvent(new ConnectivityEventInfo(tenantId, deviceId, disconnectTime) {
@Override
void forwardToLocalService() {
deviceStateService.ifPresent(service -> service.onDeviceDisconnect(tenantId, deviceId, disconnectTime));
}
@Override
TransportProtos.ToCoreMsg toQueueMsg() {
var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastDisconnectTime(disconnectTime)
.build();
return TransportProtos.ToCoreMsg.newBuilder()
.setDeviceDisconnectMsg(deviceDisconnectMsg)
.build();
}
}, callback);
}
@Override
public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) {
routeEvent(new ConnectivityEventInfo(tenantId, deviceId, inactivityTime) {
@Override
void forwardToLocalService() {
deviceStateService.ifPresent(service -> service.onDeviceInactivity(tenantId, deviceId, inactivityTime));
}
@Override
TransportProtos.ToCoreMsg toQueueMsg() {
var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastInactivityTime(inactivityTime)
.build();
return TransportProtos.ToCoreMsg.newBuilder()
.setDeviceInactivityMsg(deviceInactivityMsg)
.build();
}
}, callback);
}
private void routeEvent(ConnectivityEventInfo eventInfo, TbCallback callback) {
var tenantId = eventInfo.getTenantId();
var deviceId = eventInfo.getDeviceId();
long eventTime = eventInfo.getEventTime();
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) {
log.debug("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime);
try {
eventInfo.forwardToLocalService();
} catch (Exception e) {
log.error("[{}][{}] Failed to process device connectivity event. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime, e);
callback.onFailure(e);
return;
}
callback.onSuccess();
} else {
TransportProtos.ToCoreMsg msg = eventInfo.toQueueMsg();
log.debug("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime);
clusterService.pushMsgToCore(tpi, UUID.randomUUID(), msg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure));
}
}
}

14
application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java

@ -27,11 +27,21 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
*/ */
public interface DeviceStateService extends ApplicationListener<PartitionChangeEvent> { public interface DeviceStateService extends ApplicationListener<PartitionChangeEvent> {
void onDeviceConnect(TenantId tenantId, DeviceId deviceId); void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long lastConnectTime);
default void onDeviceConnect(TenantId tenantId, DeviceId deviceId) {
onDeviceConnect(tenantId, deviceId, System.currentTimeMillis());
}
void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime); void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime);
void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId); void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long lastDisconnectTime);
default void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) {
onDeviceDisconnect(tenantId, deviceId, System.currentTimeMillis());
}
void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long lastInactivityTime);
void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout); void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout);

2
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java

@ -36,8 +36,6 @@ public abstract class TbSubscription<T> {
private final TbSubscriptionType type; private final TbSubscriptionType type;
private final BiConsumer<TbSubscription<T>, T> updateProcessor; private final BiConsumer<TbSubscription<T>, T> updateProcessor;
protected final AtomicInteger sequence = new AtomicInteger();
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;

15
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java

@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.notification.rule.EscalatedNotificatio
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
@ -65,13 +67,24 @@ public class NotificationRuleExportService<I extends EntityId, E extends Exporta
} }
break; break;
} }
case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: {
RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig;
Set<UUID> ruleChains = triggerConfig.getRuleChains(); Set<UUID> ruleChains = triggerConfig.getRuleChains();
if (ruleChains != null) { if (ruleChains != null) {
triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet())); triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet()));
} }
break; break;
}
case EDGE_CONNECTION: {
EdgeConnectionNotificationRuleTriggerConfig triggerConfig = (EdgeConnectionNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
case EDGE_COMMUNICATION_FAILURE: {
EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig = (EdgeCommunicationFailureNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
} }
NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig();

15
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java

@ -33,6 +33,8 @@ import org.thingsboard.server.common.data.notification.rule.EscalatedNotificatio
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
@ -86,7 +88,7 @@ public class NotificationRuleImportService extends BaseEntityImportService<Notif
} }
break; break;
} }
case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: {
RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig;
Set<UUID> ruleChains = triggerConfig.getRuleChains(); Set<UUID> ruleChains = triggerConfig.getRuleChains();
if (ruleChains != null) { if (ruleChains != null) {
@ -95,6 +97,17 @@ public class NotificationRuleImportService extends BaseEntityImportService<Notif
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
break; break;
}
case EDGE_CONNECTION: {
EdgeConnectionNotificationRuleTriggerConfig triggerConfig = (EdgeConnectionNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
case EDGE_COMMUNICATION_FAILURE: {
EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig = (EdgeCommunicationFailureNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
} }
if (!triggerType.isTenantLevel()) { if (!triggerType.isTenantLevel()) {
throw new IllegalArgumentException("Trigger type " + triggerType + " is not available for tenants"); throw new IllegalArgumentException("Trigger type " + triggerType + " is not available for tenants");

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

@ -259,13 +259,13 @@ public class DefaultWebSocketService implements WebSocketService {
} }
@Override @Override
public void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update) { public void sendUpdate(String sessionId, int cmdId, TelemetrySubscriptionUpdate update) {
sendUpdate(sessionId, update.getSubscriptionId(), update); doSendUpdate(sessionId, cmdId, update);
} }
@Override @Override
public void sendUpdate(String sessionId, CmdUpdate update) { public void sendUpdate(String sessionId, CmdUpdate update) {
sendUpdate(sessionId, update.getCmdId(), update); doSendUpdate(sessionId, update.getCmdId(), update);
} }
@Override @Override
@ -274,7 +274,7 @@ public class DefaultWebSocketService implements WebSocketService {
sendUpdate(sessionRef, update); sendUpdate(sessionRef, update);
} }
private <T> void sendUpdate(String sessionId, int cmdId, T update) { private <T> void doSendUpdate(String sessionId, int cmdId, T update) {
WsSessionMetaData md = wsSessionsMap.get(sessionId); WsSessionMetaData md = wsSessionsMap.get(sessionId);
if (md != null) { if (md != null) {
sendUpdate(md.getSessionRef(), cmdId, update); sendUpdate(md.getSessionRef(), cmdId, update);
@ -288,7 +288,7 @@ public class DefaultWebSocketService implements WebSocketService {
try { try {
msgEndpoint.close(md.getSessionRef(), status); msgEndpoint.close(md.getSessionRef(), status);
} catch (IOException e) { } catch (IOException e) {
log.warn("[{}] Failed to send session close: {}", sessionId, e); log.warn("[{}] Failed to send session close", sessionId, e);
} }
} }
} }
@ -439,7 +439,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.queryTs(queryTs) .queryTs(queryTs)
@ -449,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -545,7 +545,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.queryTs(queryTs) .queryTs(queryTs)
@ -554,7 +554,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -643,13 +643,13 @@ public class DefaultWebSocketService implements WebSocketService {
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -698,13 +698,13 @@ public class DefaultWebSocketService implements WebSocketService {
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -836,7 +836,7 @@ public class DefaultWebSocketService implements WebSocketService {
try { try {
msgEndpoint.sendPing(md.getSessionRef(), currentTime); msgEndpoint.sendPing(md.getSessionRef(), currentTime);
} catch (IOException e) { } catch (IOException e) {
log.warn("[{}] Failed to send ping: {}", md.getSessionRef().getSessionId(), e); log.warn("[{}] Failed to send ping:", md.getSessionRef().getSessionId(), e);
} }
})); }));
} }

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

@ -29,7 +29,7 @@ public interface WebSocketService {
void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper); void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper);
void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update); void sendUpdate(String sessionId, int cmdId, TelemetrySubscriptionUpdate update);
void sendUpdate(String sessionId, CmdUpdate update); void sendUpdate(String sessionId, CmdUpdate update);

8
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -115,7 +115,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId());
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId()); int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId());
subscription.getUnreadCounter().set(unreadCount); subscription.getTotalUnreadCounter().set(unreadCount);
} }
@ -196,20 +196,20 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) { private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
if (update.isCreated()) { if (update.isCreated()) {
subscription.getUnreadCounter().incrementAndGet(); subscription.getTotalUnreadCounter().incrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createUpdate()); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} else if (update.isUpdated()) { } else if (update.isUpdated()) {
if (update.getNewStatus() == NotificationStatus.READ) { if (update.getNewStatus() == NotificationStatus.READ) {
if (update.isAllNotifications()) { if (update.isAllNotifications()) {
fetchUnreadNotificationsCount(subscription); fetchUnreadNotificationsCount(subscription);
} else { } else {
subscription.getUnreadCounter().decrementAndGet(); subscription.getTotalUnreadCounter().decrementAndGet();
} }
sendUpdate(subscription.getSessionId(), subscription.createUpdate()); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} }
} else if (update.isDeleted()) { } else if (update.isDeleted()) {
if (update.getNotification().getStatus() != NotificationStatus.READ) { if (update.getNotification().getStatus() != NotificationStatus.READ) {
subscription.getUnreadCounter().decrementAndGet(); subscription.getTotalUnreadCounter().decrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createUpdate()); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} }
} }

38
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.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.service.ws.notification.sub;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.subscription.TbSubscription;
import org.thingsboard.server.service.subscription.TbSubscriptionType;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@Getter
public abstract class AbstractNotificationSubscription<T> extends TbSubscription<T> {
protected final AtomicInteger sequence = new AtomicInteger();
protected final AtomicInteger totalUnreadCounter = new AtomicInteger();
public AbstractNotificationSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, TbSubscriptionType type, BiConsumer<TbSubscription<T>, T> updateProcessor) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, type, updateProcessor);
}
}

6
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java

@ -27,9 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@Getter @Getter
public class NotificationsCountSubscription extends TbSubscription<NotificationsSubscriptionUpdate> { public class NotificationsCountSubscription extends AbstractNotificationSubscription<NotificationsSubscriptionUpdate> {
private final AtomicInteger unreadCounter = new AtomicInteger();
@Builder @Builder
public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
@ -40,7 +38,7 @@ public class NotificationsCountSubscription extends TbSubscription<Notifications
public UnreadNotificationsCountUpdate createUpdate() { public UnreadNotificationsCountUpdate createUpdate() {
return UnreadNotificationsCountUpdate.builder() return UnreadNotificationsCountUpdate.builder()
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.totalUnreadCount(unreadCounter.get()) .totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet()) .sequenceNumber(sequence.incrementAndGet())
.build(); .build();
} }

3
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java

@ -35,11 +35,10 @@ import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Getter @Getter
public class NotificationsSubscription extends TbSubscription<NotificationsSubscriptionUpdate> { public class NotificationsSubscription extends AbstractNotificationSubscription<NotificationsSubscriptionUpdate> {
private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>(); private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>();
private final int limit; private final int limit;
private final AtomicInteger totalUnreadCounter = new AtomicInteger();
@Builder @Builder
public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,

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

@ -786,7 +786,7 @@ state:
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}" defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}"
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}" # Interval for checking the device state after a specified period. Time in seconds defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}" # Interval for checking the device state after a specified period. Time in seconds
# Controls whether we store the device 'active' flag in attributes (default) or telemetry. # Controls whether we store the device 'active' flag in attributes (default) or telemetry.
# If you device to change this parameter, you should re-create the device info view as one of the following: # If you decide to change this parameter, you should re-create the device info view as one of the following:
# If 'persistToTelemetry' is changed from 'false' to 'true': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_ts_view;' # If 'persistToTelemetry' is changed from 'false' to 'true': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_ts_view;'
# If 'persistToTelemetry' is changed from 'true' to 'false': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;' # If 'persistToTelemetry' is changed from 'true' to 'false': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;'
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}" persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
@ -794,6 +794,15 @@ state:
# Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data. # Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data.
# 0 means time-to-live mechanism is disabled. # 0 means time-to-live mechanism is disabled.
telemetryTtl: "${STATE_TELEMETRY_TTL:0}" telemetryTtl: "${STATE_TELEMETRY_TTL:0}"
# Configuration properties for rule nodes related to device activity state
rule:
node:
# Device state rule node
deviceState:
# Defines the rate at which device connectivity events can be triggered.
# Comma-separated list of capacity:duration pairs that define bandwidth capacity and refill duration for token bucket rate limit algorithm.
# Refill is set to be greedy. Please refer to Bucket4j library documentation for more details.
rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}"
# Tbel parameters # Tbel parameters
tbel: tbel:
@ -1044,6 +1053,8 @@ transport:
dtls: dtls:
# RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000
retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}"
# "" disables connection id support, 0 enables support but not for incoming traffic, any value greater than 0 set the connection id size in bytes
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:6}"
server: server:
# LwM2M Server ID # LwM2M Server ID
id: "${LWM2M_SERVER_ID:123}" id: "${LWM2M_SERVER_ID:123}"
@ -1173,14 +1184,18 @@ transport:
bind_port: "${SNMP_BIND_PORT:1620}" bind_port: "${SNMP_BIND_PORT:1620}"
response_processing: response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:4}"
# to configure SNMP to work over UDP or TCP # to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
# Batch size to request OID mappings from the device (useful when the device profile has multiple hundreds of OID mappings) # Maximum size of a PDU (amount of OID mappings in a single SNMP request). The request will be split into multiple PDUs if mappings amount exceeds this number
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
# Delay after sending each request chunk (in case the request was split into multiple PDUs due to max_request_oids)
request_chunk_delay_ms: "${SNMP_REQUEST_CHUNK_DELAY_MS:100}"
response: response:
# To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types) # To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types)
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
# Thread pool size for scheduler that executes device querying tasks
scheduler_thread_pool_size: "${SNMP_SCHEDULER_THREAD_POOL_SIZE:4}"
stats: stats:
# Enable/Disable the collection of transport statistics # Enable/Disable the collection of transport statistics
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"

20
application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java

@ -154,24 +154,6 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
Mockito.reset(tbClusterService, auditLogService); Mockito.reset(tbClusterService, auditLogService);
} }
protected void testNotifyManyEntityManyTimeMsgToEdgeServiceNever(HasName entity, HasName originator,
TenantId tenantId, CustomerId customerId, UserId userId, String userName,
ActionType actionType, int cntTime, Object... additionalInfo) {
EntityId entityId = createEntityId_NULL_UUID(entity);
EntityId originatorId = createEntityId_NULL_UUID(originator);
testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType);
ArgumentMatcher<HasName> matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass());
ArgumentMatcher<EntityId> matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass());
ArgumentMatcher<CustomerId> matcherCustomerId = customerId == null ?
argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId);
ArgumentMatcher<UserId> matcherUserId = userId == null ?
argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId);
testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime,
extractMatcherAdditionalInfo(additionalInfo));
testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime);
Mockito.reset(tbClusterService, auditLogService);
}
protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(HasName entity, HasName originator, protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(HasName entity, HasName originator,
TenantId tenantId, CustomerId customerId, UserId userId, String userName, TenantId tenantId, CustomerId customerId, UserId userId, String userName,
ActionType actionType, ActionType actionType,
@ -624,7 +606,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
private String entityClassToString(HasName entity) { private String entityClassToString(HasName entity) {
String className = entity.getClass().toString() String className = entity.getClass().toString()
.substring(entity.getClass().toString().lastIndexOf(".") + 1); .substring(entity.getClass().toString().lastIndexOf(".") + 1);
List str = className.chars() List<String> str = className.chars()
.mapToObj(x -> (Character.isUpperCase(x)) ? "_" + Character.toString(x) : Character.toString(x)) .mapToObj(x -> (Character.isUpperCase(x)) ? "_" + Character.toString(x) : Character.toString(x))
.collect(Collectors.toList()); .collect(Collectors.toList());
return String.join("", str).toUpperCase(Locale.ENGLISH).substring(1); return String.join("", str).toUpperCase(Locale.ENGLISH).substring(1);

37
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -711,7 +711,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(30, TimeUnit.SECONDS)); Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(TIMEOUT, TimeUnit.SECONDS));
Assert.assertEquals(JacksonUtil.newObjectNode().put(attrKey, attrValue), Assert.assertEquals(JacksonUtil.newObjectNode().put(attrKey, attrValue),
JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes())); JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes()));
@ -798,7 +798,21 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
// clean up stored edge events // clean up stored edge events
edgeEventService.cleanupEvents(1); edgeEventService.cleanupEvents(1);
// perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly // edge is disconnected: perform rpc call - no edge event saved
doPostAsync(
"/api/rpc/oneway/" + device.getId().getId().toString(),
JacksonUtil.toString(createDefaultRpc()),
String.class,
status().isOk());
Awaitility.await()
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
PageData<EdgeEvent> result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1));
return result.getTotalElements() == 0;
});
// edge is connected: perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly
simulateEdgeActivation(tmpEdge);
doPostAsync( doPostAsync(
"/api/rpc/oneway/" + device.getId().getId().toString(), "/api/rpc/oneway/" + device.getId().getId().toString(),
JacksonUtil.toString(createDefaultRpc()), JacksonUtil.toString(createDefaultRpc()),
@ -857,4 +871,23 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
return rpc; return rpc;
} }
private void simulateEdgeActivation(Edge edge) throws Exception {
ObjectNode attributes = JacksonUtil.newObjectNode();
attributes.put("active", true);
doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes);
Awaitility.await()
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
List<Map<String, Object>> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {});
Optional<Map<String, Object>> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst();
if (activeAttrOpt.isEmpty()) {
return false;
}
Map<String, Object> activeAttr = activeAttrOpt.get();
return "true".equals(activeAttr.get("value").toString());
});
}
} }

23
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -146,7 +146,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
} }
} }
private void createRuleChainMetadata(RuleChain ruleChain) { private RuleChainMetaData createRuleChainMetadata(RuleChain ruleChain) {
RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
ruleChainMetaData.setRuleChainId(ruleChain.getId()); ruleChainMetaData.setRuleChainId(ruleChain.getId());
@ -182,7 +182,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
ruleChainMetaData.addConnectionInfo(0, 2, "fail"); ruleChainMetaData.addConnectionInfo(0, 2, "fail");
ruleChainMetaData.addConnectionInfo(1, 2, "success"); ruleChainMetaData.addConnectionInfo(1, 2, "success");
doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); return doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
} }
@Test @Test
@ -193,9 +193,10 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
ruleChain.setType(RuleChainType.EDGE); ruleChain.setType(RuleChainType.EDGE);
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class);
RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
// set new rule chain as root // set new rule chain as root
@ -213,6 +214,22 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(ruleChainMsg.isRoot()); Assert.assertTrue(ruleChainMsg.isRoot());
Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId());
// update metadata for root rule chain
edgeImitator.expectMessageAmount(1);
metaData.getNodes().forEach(n -> n.setDebugMode(true));
doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
Assert.assertTrue(edgeImitator.waitForMessages());
ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class);
Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent());
ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get();
ruleChainMsg = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true);
Assert.assertNotNull(ruleChainMsg);
Assert.assertTrue(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()) ||
UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()));
Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId());
Assert.assertEquals(savedRuleChain.getName(), ruleChainMsg.getName());
Assert.assertTrue(ruleChainMsg.isRoot());
// revert root rule chain // revert root rule chain
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()

7
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.edge.rpc.EdgeGrpcClient; import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
@ -72,8 +73,6 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class EdgeImitator { public class EdgeImitator {
public static final int TIMEOUT_IN_SECONDS = 30;
private String routingKey; private String routingKey;
private String routingSecret; private String routingSecret;
@ -344,7 +343,7 @@ public class EdgeImitator {
} }
public boolean waitForMessages() throws InterruptedException { public boolean waitForMessages() throws InterruptedException {
return waitForMessages(TIMEOUT_IN_SECONDS); return waitForMessages(AbstractWebTest.TIMEOUT);
} }
public boolean waitForMessages(int timeoutInSeconds) throws InterruptedException { public boolean waitForMessages(int timeoutInSeconds) throws InterruptedException {
@ -359,7 +358,7 @@ public class EdgeImitator {
} }
public boolean waitForResponses() throws InterruptedException { public boolean waitForResponses() throws InterruptedException {
return responsesLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); return responsesLatch.await(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS);
} }
public void expectResponsesAmount(int messageAmount) { public void expectResponsesAmount(int messageAmount) {

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

@ -315,7 +315,7 @@ public class HashPartitionServiceTest {
.setPartitions(isolatedQueue.getPartitions()) .setPartitions(isolatedQueue.getPartitions())
.build(); .build();
partitionService_common.updateQueue(queueUpdateMsg); partitionService_common.updateQueues(List.of(queueUpdateMsg));
partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine)); partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine));
// expecting event about no partitions for isolated queue key // expecting event about no partitions for isolated queue key
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
@ -323,7 +323,7 @@ public class HashPartitionServiceTest {
return event.getPartitionsMap().get(queueKey).isEmpty(); return event.getPartitionsMap().get(queueKey).isEmpty();
}); });
partitionService_dedicated.updateQueue(queueUpdateMsg); partitionService_dedicated.updateQueues(List.of(queueUpdateMsg));
partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine)); partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine));
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId);
@ -342,7 +342,7 @@ public class HashPartitionServiceTest {
.setQueueIdLSB(isolatedQueue.getUuidId().getLeastSignificantBits()) .setQueueIdLSB(isolatedQueue.getUuidId().getLeastSignificantBits())
.setQueueName(isolatedQueue.getName()) .setQueueName(isolatedQueue.getName())
.build(); .build();
partitionService_dedicated.removeQueue(queueDeleteMsg); partitionService_dedicated.removeQueues(List.of(queueDeleteMsg));
verifyPartitionChangeEvent(event -> { verifyPartitionChangeEvent(event -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId);
return event.getPartitionsMap().get(queueKey).isEmpty(); return event.getPartitionsMap().get(queueKey).isEmpty();

87
application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.edge.rpc.constructor; package org.thingsboard.server.service.edge.rpc.constructor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
@ -61,7 +60,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() {
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(
ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); ruleChainId, 3, createRuleNodes(ruleChainId), createConnections());
@ -80,7 +79,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() {
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(
ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); ruleChainId, 3, createRuleNodes(ruleChainId), createConnections());
@ -120,7 +119,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() {
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections());
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg =
@ -161,7 +160,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() {
// same rule chain metadata, but different order of rule nodes // same rule chain metadata, but different order of rule nodes
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder()); RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder());
@ -254,7 +253,7 @@ public class RuleChainMsgConstructorTest {
return result; return result;
} }
private List<RuleNode> createRuleNodes(RuleChainId ruleChainId) throws JsonProcessingException { private List<RuleNode> createRuleNodes(RuleChainId ruleChainId) {
List<RuleNode> result = new ArrayList<>(); List<RuleNode> result = new ArrayList<>();
result.add(getOutputNode(ruleChainId)); result.add(getOutputNode(ruleChainId));
result.add(getAcknowledgeNode(ruleChainId)); result.add(getAcknowledgeNode(ruleChainId));
@ -301,7 +300,7 @@ public class RuleChainMsgConstructorTest {
return result; return result;
} }
private List<RuleNode> createRuleNodesInDifferentOrder(RuleChainId ruleChainId) throws JsonProcessingException { private List<RuleNode> createRuleNodesInDifferentOrder(RuleChainId ruleChainId) {
List<RuleNode> result = new ArrayList<>(); List<RuleNode> result = new ArrayList<>();
result.add(getPushToAnalyticsNode(ruleChainId)); result.add(getPushToAnalyticsNode(ruleChainId));
result.add(getPushToCloudNode(ruleChainId)); result.add(getPushToCloudNode(ruleChainId));
@ -319,99 +318,99 @@ public class RuleChainMsgConstructorTest {
} }
private RuleNode getOutputNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getOutputNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbRuleChainOutputNode", "org.thingsboard.rule.engine.flow.TbRuleChainOutputNode",
"Output node", "Output node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), JacksonUtil.toJsonNode("{\"version\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}"));
} }
private RuleNode getCheckpointNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getCheckpointNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbCheckpointNode", "org.thingsboard.rule.engine.flow.TbCheckpointNode",
"Checkpoint node", "Checkpoint node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"queueName\":\"HighPriority\"}"), JacksonUtil.toJsonNode("{\"queueName\":\"HighPriority\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}"));
} }
private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"Save Timeseries", "Save Timeseries",
JacksonUtil.OBJECT_MAPPER.readTree("{\"defaultTTL\":0}"), JacksonUtil.toJsonNode("{\"defaultTTL\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":823,\"layoutY\":157}")); JacksonUtil.toJsonNode("{\"layoutX\":823,\"layoutY\":157}"));
} }
private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"Message Type Switch", "Message Type Switch",
JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), JacksonUtil.toJsonNode("{\"version\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":347,\"layoutY\":149}")); JacksonUtil.toJsonNode("{\"layoutX\":347,\"layoutY\":149}"));
} }
private RuleNode getLogOtherNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getLogOtherNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.action.TbLogNode", "org.thingsboard.rule.engine.action.TbLogNode",
"Log Other", "Log Other",
JacksonUtil.OBJECT_MAPPER.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":378}")); JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":378}"));
} }
private RuleNode getPushToCloudNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getPushToCloudNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode",
"Push to cloud", "Push to cloud",
JacksonUtil.OBJECT_MAPPER.readTree("{\"scope\":\"SERVER_SCOPE\"}"), JacksonUtil.toJsonNode("{\"scope\":\"SERVER_SCOPE\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":1129,\"layoutY\":52}")); JacksonUtil.toJsonNode("{\"layoutX\":1129,\"layoutY\":52}"));
} }
private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbAckNode", "org.thingsboard.rule.engine.flow.TbAckNode",
"Acknowledge node", "Acknowledge node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), JacksonUtil.toJsonNode("{\"version\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}"));
} }
private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"Device Profile Node", "Device Profile Node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"), JacksonUtil.toJsonNode("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}")); JacksonUtil.toJsonNode("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}"));
} }
private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"Save Client Attributes", "Save Client Attributes",
JacksonUtil.OBJECT_MAPPER.readTree("{\"scope\":\"CLIENT_SCOPE\"}"), JacksonUtil.toJsonNode("{\"scope\":\"CLIENT_SCOPE\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":52}")); JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":52}"));
} }
private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.action.TbLogNode", "org.thingsboard.rule.engine.action.TbLogNode",
"Log RPC from Device", "Log RPC from Device",
JacksonUtil.OBJECT_MAPPER.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":825,\"layoutY\":266}")); JacksonUtil.toJsonNode("{\"layoutX\":825,\"layoutY\":266}"));
} }
private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"RPC Call Request", "RPC Call Request",
JacksonUtil.OBJECT_MAPPER.readTree("{\"timeoutInSeconds\":60}"), JacksonUtil.toJsonNode("{\"timeoutInSeconds\":60}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":466}")); JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":466}"));
} }
private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbRuleChainInputNode", "org.thingsboard.rule.engine.flow.TbRuleChainInputNode",
"Push to Analytics", "Push to Analytics",
JacksonUtil.OBJECT_MAPPER.readTree("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"), JacksonUtil.toJsonNode("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}"));
} }
} }

4
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java

@ -55,6 +55,7 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
@ -209,6 +210,9 @@ public abstract class BaseEdgeProcessorTest {
@MockBean @MockBean
protected AttributesService attributesService; protected AttributesService attributesService;
@MockBean
protected TimeseriesService timeseriesService;
@MockBean @MockBean
protected TbClusterService tbClusterService; protected TbClusterService tbClusterService;

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

@ -20,6 +20,8 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.server.cache.limits.DefaultRateLimitService;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationRuleId;
@ -28,9 +30,7 @@ import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.DefaultTbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.DefaultRateLimitService;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -46,12 +46,12 @@ import static org.mockito.Mockito.when;
public class RateLimitServiceTest { public class RateLimitServiceTest {
private RateLimitService rateLimitService; private RateLimitService rateLimitService;
private TbTenantProfileCache tenantProfileCache; private DefaultTbTenantProfileCache tenantProfileCache;
private TenantId tenantId; private TenantId tenantId;
@Before @Before
public void beforeEach() { public void beforeEach() {
tenantProfileCache = Mockito.mock(TbTenantProfileCache.class); tenantProfileCache = Mockito.mock(DefaultTbTenantProfileCache.class);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100); rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
tenantId = new TenantId(UUID.randomUUID()); tenantId = new TenantId(UUID.randomUUID());
} }
@ -69,6 +69,8 @@ public class RateLimitServiceTest {
profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit); profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit);
profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit); profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit);
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit);
profileConfiguration.setEdgeEventRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit);
updateTenantProfileConfiguration(profileConfiguration); updateTenantProfileConfiguration(profileConfiguration);
for (LimitedApi limitedApi : List.of( for (LimitedApi limitedApi : List.of(
@ -76,7 +78,9 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_IMPORT, LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS, LimitedApi.NOTIFICATION_REQUESTS,
LimitedApi.REST_REQUESTS_PER_CUSTOMER, LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES LimitedApi.CASSANDRA_QUERIES,
LimitedApi.EDGE_EVENTS,
LimitedApi.EDGE_EVENTS_PER_EDGE
)) { )) {
testRateLimits(limitedApi, max, tenantId); testRateLimits(limitedApi, max, tenantId);
} }

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

@ -93,7 +93,7 @@ import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService; import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService;
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache; import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.state.DeviceStateService;

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

@ -41,6 +41,7 @@ import org.thingsboard.server.service.gateway_device.GatewayNotificationsService
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -95,7 +96,7 @@ public class DefaultTbClusterServiceTest {
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer); when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer);
clusterService.onQueueChange(createTestQueue()); clusterService.onQueuesUpdate(List.of(createTestQueue()));
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH);
verify(topicService, never()).getNotificationsTopic(eq(ServiceType.TB_CORE), any()); verify(topicService, never()).getNotificationsTopic(eq(ServiceType.TB_CORE), any());
@ -120,7 +121,7 @@ public class DefaultTbClusterServiceTest {
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer); when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer);
clusterService.onQueueChange(createTestQueue()); clusterService.onQueuesUpdate(List.of(createTestQueue()));
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1);
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2);
@ -148,7 +149,7 @@ public class DefaultTbClusterServiceTest {
when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbREQueueProducer); when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbREQueueProducer);
when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer); when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer);
clusterService.onQueueChange(createTestQueue()); clusterService.onQueuesUpdate(List.of(createTestQueue()));
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH);
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_TRANSPORT, TRANSPORT); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_TRANSPORT, TRANSPORT);
@ -194,7 +195,7 @@ public class DefaultTbClusterServiceTest {
when(producerProvider.getTbCoreNotificationsMsgProducer()).thenReturn(tbCoreQueueProducer); when(producerProvider.getTbCoreNotificationsMsgProducer()).thenReturn(tbCoreQueueProducer);
when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer); when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer);
clusterService.onQueueChange(createTestQueue()); clusterService.onQueuesUpdate(List.of(createTestQueue()));
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1);
verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2);

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

@ -0,0 +1,532 @@
/**
* 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.queue;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
@ExtendWith(MockitoExtension.class)
public class DefaultTbCoreConsumerServiceTest {
@Mock
private DeviceStateService stateServiceMock;
@Mock
private TbCoreConsumerStats statsMock;
@Mock
private TbCallback tbCallbackMock;
private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
private final DeviceId deviceId = new DeviceId(UUID.randomUUID());
private final long time = System.currentTimeMillis();
private ListeningExecutorService executor;
@Mock
private DefaultTbCoreConsumerService defaultTbCoreConsumerServiceMock;
@BeforeEach
public void setup() {
executor = MoreExecutors.newDirectExecutorService();
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stateService", stateServiceMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "deviceActivityEventsExecutor", executor);
}
@AfterEach
public void cleanup() {
if (executor != null) {
executor.shutdown();
try {
if (!executor.awaitTermination(10L, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@Test
public void givenProcessingSuccess_whenForwardingDeviceStateMsgToStateService_thenOnSuccessCallbackIsCalled() {
// GIVEN
var stateMsg = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(stateMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(stateMsg, tbCallbackMock);
// THEN
then(stateServiceMock).should().onQueueMsg(stateMsg, tbCallbackMock);
}
@Test
public void givenStatsEnabled_whenForwardingDeviceStateMsgToStateService_thenStatsAreRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true);
var stateMsg = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(stateMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(stateMsg, tbCallbackMock);
// THEN
then(statsMock).should().log(stateMsg);
}
@Test
public void givenStatsDisabled_whenForwardingDeviceStateMsgToStateService_thenStatsAreNotRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false);
var stateMsg = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(stateMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(stateMsg, tbCallbackMock);
// THEN
then(statsMock).should(never()).log(stateMsg);
}
@Test
public void givenProcessingSuccess_whenForwardingConnectMsgToStateService_thenOnSuccessCallbackIsCalled() {
// GIVEN
var connectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastConnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock);
// THEN
then(stateServiceMock).should().onDeviceConnect(tenantId, deviceId, time);
then(tbCallbackMock).should().onSuccess();
then(tbCallbackMock).should(never()).onFailure(any());
}
@Test
public void givenProcessingFailure_whenForwardingConnectMsgToStateService_thenOnFailureCallbackIsCalled() {
// GIVEN
var connectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastConnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock);
var runtimeException = new RuntimeException("Something bad happened!");
doThrow(runtimeException).when(stateServiceMock).onDeviceConnect(tenantId, deviceId, time);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock);
// THEN
then(tbCallbackMock).should(never()).onSuccess();
then(tbCallbackMock).should().onFailure(runtimeException);
}
@Test
public void givenStatsEnabled_whenForwardingConnectMsgToStateService_thenStatsAreRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true);
var connectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastConnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock);
// THEN
then(statsMock).should().log(connectMsg);
}
@Test
public void givenStatsDisabled_whenForwardingConnectMsgToStateService_thenStatsAreNotRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false);
var connectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastConnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock);
// THEN
then(statsMock).should(never()).log(connectMsg);
}
@Test
public void givenProcessingSuccess_whenForwardingActivityMsgToStateService_thenOnSuccessCallbackIsCalled() {
// GIVEN
var activityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastActivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock);
// THEN
then(stateServiceMock).should().onDeviceActivity(tenantId, deviceId, time);
then(tbCallbackMock).should().onSuccess();
then(tbCallbackMock).should(never()).onFailure(any());
}
@Test
public void givenProcessingFailure_whenForwardingActivityMsgToStateService_thenOnFailureCallbackIsCalled() {
// GIVEN
var activityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastActivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock);
var runtimeException = new RuntimeException("Something bad happened!");
doThrow(runtimeException).when(stateServiceMock).onDeviceActivity(tenantId, deviceId, time);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock);
// THEN
then(tbCallbackMock).should(never()).onSuccess();
var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
then(tbCallbackMock).should().onFailure(exceptionCaptor.capture());
assertThat(exceptionCaptor.getValue())
.isInstanceOf(RuntimeException.class)
.hasMessage("Failed to update device activity for device [" + deviceId.getId() + "]!")
.hasCause(runtimeException);
}
@Test
public void givenStatsEnabled_whenForwardingActivityMsgToStateService_thenStatsAreRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true);
var activityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastActivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock);
// THEN
then(statsMock).should().log(activityMsg);
}
@Test
public void givenStatsDisabled_whenForwardingActivityMsgToStateService_thenStatsAreNotRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false);
var activityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastActivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock);
// THEN
then(statsMock).should(never()).log(activityMsg);
}
@Test
public void givenProcessingSuccess_whenForwardingDisconnectMsgToStateService_thenOnSuccessCallbackIsCalled() {
// GIVEN
var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastDisconnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock);
// THEN
then(stateServiceMock).should().onDeviceDisconnect(tenantId, deviceId, time);
then(tbCallbackMock).should().onSuccess();
then(tbCallbackMock).should(never()).onFailure(any());
}
@Test
public void givenProcessingFailure_whenForwardingDisconnectMsgToStateService_thenOnFailureCallbackIsCalled() {
// GIVEN
var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastDisconnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock);
var runtimeException = new RuntimeException("Something bad happened!");
doThrow(runtimeException).when(stateServiceMock).onDeviceDisconnect(tenantId, deviceId, time);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock);
// THEN
then(tbCallbackMock).should(never()).onSuccess();
then(tbCallbackMock).should().onFailure(runtimeException);
}
@Test
public void givenStatsEnabled_whenForwardingDisconnectMsgToStateService_thenStatsAreRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true);
var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastDisconnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock);
// THEN
then(statsMock).should().log(disconnectMsg);
}
@Test
public void givenStatsDisabled_whenForwardingDisconnectMsgToStateService_thenStatsAreNotRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false);
var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastDisconnectTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock);
// THEN
then(statsMock).should(never()).log(disconnectMsg);
}
@Test
public void givenProcessingSuccess_whenForwardingInactivityMsgToStateService_thenOnSuccessCallbackIsCalled() {
// GIVEN
var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastInactivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock);
// THEN
then(stateServiceMock).should().onDeviceInactivity(tenantId, deviceId, time);
then(tbCallbackMock).should().onSuccess();
then(tbCallbackMock).should(never()).onFailure(any());
}
@Test
public void givenProcessingFailure_whenForwardingInactivityMsgToStateService_thenOnFailureCallbackIsCalled() {
// GIVEN
var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastInactivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock);
var runtimeException = new RuntimeException("Something bad happened!");
doThrow(runtimeException).when(stateServiceMock).onDeviceInactivity(tenantId, deviceId, time);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock);
// THEN
then(tbCallbackMock).should(never()).onSuccess();
then(tbCallbackMock).should().onFailure(runtimeException);
}
@Test
public void givenStatsEnabled_whenForwardingInactivityMsgToStateService_thenStatsAreRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true);
var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastInactivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock);
// THEN
then(statsMock).should().log(inactivityMsg);
}
@Test
public void givenStatsDisabled_whenForwardingInactivityMsgToStateService_thenStatsAreNotRecorded() {
// GIVEN
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock);
ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false);
var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setLastInactivityTime(time)
.build();
doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock);
// WHEN
defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock);
// THEN
then(statsMock).should(never()).log(inactivityMsg);
}
}

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

@ -21,11 +21,13 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -54,19 +56,26 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
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.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
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.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@ -74,6 +83,8 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACT
import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_ALARM_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_ALARM_TIME;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT; import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_ACTIVITY_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_ACTIVITY_TIME;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class DefaultDeviceStateServiceTest { public class DefaultDeviceStateServiceTest {
@ -116,21 +127,366 @@ public class DefaultDeviceStateServiceTest {
tpi = TopicPartitionInfo.builder().myPartition(true).build(); tpi = TopicPartitionInfo.builder().myPartition(true).build();
} }
@Test
public void givenDeviceBelongsToExternalPartition_whenOnDeviceConnect_thenCleansStateAndDoesNotReportConnect() {
// GIVEN
doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
// WHEN
service.onDeviceConnect(tenantId, deviceId, System.currentTimeMillis());
// THEN
then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
then(service).should(never()).getOrFetchDeviceStateData(deviceId);
then(service).should(never()).checkAndUpdateState(eq(deviceId), any());
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@ValueSource(longs = {Long.MIN_VALUE, -100, -1})
public void givenNegativeLastConnectTime_whenOnDeviceConnect_thenSkipsThisEvent(long negativeLastConnectTime) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
// WHEN
service.onDeviceConnect(tenantId, deviceId, negativeLastConnectTime);
// THEN
then(service).should(never()).getOrFetchDeviceStateData(deviceId);
then(service).should(never()).checkAndUpdateState(eq(deviceId), any());
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@MethodSource("provideOutdatedTimestamps")
public void givenOutdatedLastConnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastConnectTime, long currentLastConnectTime) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().lastConnectTime(currentLastConnectTime).build())
.build();
service.deviceStates.put(deviceId, deviceStateData);
// WHEN
service.onDeviceConnect(tenantId, deviceId, outdatedLastConnectTime);
// THEN
then(service).should(never()).checkAndUpdateState(eq(deviceId), any());
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@Test
public void givenDeviceBelongsToMyPartition_whenOnDeviceConnect_thenReportsConnect() {
// GIVEN
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().build())
.metaData(new TbMsgMetaData())
.build();
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
service.deviceStates.put(deviceId, deviceStateData);
long lastConnectTime = System.currentTimeMillis();
// WHEN
service.onDeviceConnect(tenantId, deviceId, lastConnectTime);
// THEN
then(telemetrySubscriptionService).should().saveAttrAndNotify(
eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), eq(LAST_CONNECT_TIME), eq(lastConnectTime), any()
);
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any());
var actualMsg = msgCaptor.getValue();
assertThat(actualMsg.getType()).isEqualTo(TbMsgType.CONNECT_EVENT.name());
assertThat(actualMsg.getOriginator()).isEqualTo(deviceId);
}
@Test
public void givenDeviceBelongsToExternalPartition_whenOnDeviceDisconnect_thenCleansStateAndDoesNotReportDisconnect() {
// GIVEN
doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
// WHEN
service.onDeviceDisconnect(tenantId, deviceId, System.currentTimeMillis());
// THEN
then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
then(service).should(never()).getOrFetchDeviceStateData(deviceId);
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@ValueSource(longs = {Long.MIN_VALUE, -100, -1})
public void givenNegativeLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long negativeLastDisconnectTime) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
// WHEN
service.onDeviceDisconnect(tenantId, deviceId, negativeLastDisconnectTime);
// THEN
then(service).should(never()).getOrFetchDeviceStateData(deviceId);
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@MethodSource("provideOutdatedTimestamps")
public void givenOutdatedLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastDisconnectTime, long currentLastDisconnectTime) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().lastDisconnectTime(currentLastDisconnectTime).build())
.build();
service.deviceStates.put(deviceId, deviceStateData);
// WHEN
service.onDeviceDisconnect(tenantId, deviceId, outdatedLastDisconnectTime);
// THEN
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@Test
public void givenDeviceBelongsToMyPartition_whenOnDeviceDisconnect_thenReportsDisconnect() {
// GIVEN
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().build())
.metaData(new TbMsgMetaData())
.build();
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
service.deviceStates.put(deviceId, deviceStateData);
long lastDisconnectTime = System.currentTimeMillis();
// WHEN
service.onDeviceDisconnect(tenantId, deviceId, lastDisconnectTime);
// THEN
then(telemetrySubscriptionService).should().saveAttrAndNotify(
eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE),
eq(LAST_DISCONNECT_TIME), eq(lastDisconnectTime), any()
);
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any());
var actualMsg = msgCaptor.getValue();
assertThat(actualMsg.getType()).isEqualTo(TbMsgType.DISCONNECT_EVENT.name());
assertThat(actualMsg.getOriginator()).isEqualTo(deviceId);
}
@Test
public void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() {
// GIVEN
doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
// WHEN
service.onDeviceInactivity(tenantId, deviceId, System.currentTimeMillis());
// THEN
then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
then(service).should(never()).fetchDeviceStateDataUsingSeparateRequests(deviceId);
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@ValueSource(longs = {Long.MIN_VALUE, -100, -1})
public void givenNegativeLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(long negativeLastInactivityTime) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
// WHEN
service.onDeviceInactivity(tenantId, deviceId, negativeLastInactivityTime);
// THEN
then(service).should(never()).getOrFetchDeviceStateData(deviceId);
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@MethodSource("provideOutdatedTimestamps")
public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(
long outdatedLastInactivityTime, long currentLastInactivityTime
) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().lastInactivityAlarmTime(currentLastInactivityTime).build())
.build();
service.deviceStates.put(deviceId, deviceStateData);
// WHEN
service.onDeviceInactivity(tenantId, deviceId, outdatedLastInactivityTime);
// THEN
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
@ParameterizedTest
@MethodSource("provideOutdatedTimestamps")
public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentActivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(
long outdatedLastInactivityTime, long currentLastActivityTime
) {
// GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().lastActivityTime(currentLastActivityTime).build())
.build();
service.deviceStates.put(deviceId, deviceStateData);
// WHEN
service.onDeviceInactivity(tenantId, deviceId, outdatedLastInactivityTime);
// THEN
then(clusterService).shouldHaveNoInteractions();
then(notificationRuleProcessor).shouldHaveNoInteractions();
then(telemetrySubscriptionService).shouldHaveNoInteractions();
}
private static Stream<Arguments> provideOutdatedTimestamps() {
return Stream.of(
Arguments.of(0, 0),
Arguments.of(0, 100),
Arguments.of(50, 100),
Arguments.of(99, 100),
Arguments.of(100, 100)
);
}
@Test
public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() {
// GIVEN
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().build())
.metaData(new TbMsgMetaData())
.build();
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
service.deviceStates.put(deviceId, deviceStateData);
long lastInactivityTime = System.currentTimeMillis();
// WHEN
service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime);
// THEN
then(telemetrySubscriptionService).should().saveAttrAndNotify(
eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE),
eq(INACTIVITY_ALARM_TIME), eq(lastInactivityTime), any()
);
then(telemetrySubscriptionService).should().saveAttrAndNotify(
eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE),
eq(ACTIVITY_STATE), eq(false), any()
);
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(clusterService).should()
.pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any());
var actualMsg = msgCaptor.getValue();
assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name());
assertThat(actualMsg.getOriginator()).isEqualTo(deviceId);
var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class);
then(notificationRuleProcessor).should().process(notificationCaptor.capture());
var actualNotification = notificationCaptor.getValue();
assertThat(actualNotification.getTenantId()).isEqualTo(tenantId);
assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId);
assertThat(actualNotification.isActive()).isFalse();
}
@Test
public void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() {
// GIVEN
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(DeviceState.builder().build())
.metaData(new TbMsgMetaData())
.build();
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi);
// WHEN
service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData);
// THEN
then(telemetrySubscriptionService).should().saveAttrAndNotify(
eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE),
eq(INACTIVITY_ALARM_TIME), anyLong(), any()
);
then(telemetrySubscriptionService).should().saveAttrAndNotify(
eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE),
eq(ACTIVITY_STATE), eq(false), any()
);
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(clusterService).should()
.pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any());
var actualMsg = msgCaptor.getValue();
assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name());
assertThat(actualMsg.getOriginator()).isEqualTo(deviceId);
var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class);
then(notificationRuleProcessor).should().process(notificationCaptor.capture());
var actualNotification = notificationCaptor.getValue();
assertThat(actualNotification.getTenantId()).isEqualTo(tenantId);
assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId);
assertThat(actualNotification.isActive()).isFalse();
}
@Test @Test
public void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() { public void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() {
service.deviceStates.put(deviceId, deviceStateDataMock); service.deviceStates.put(deviceId, deviceStateDataMock);
DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId);
assertThat(deviceStateData).isEqualTo(deviceStateDataMock); assertThat(deviceStateData).isEqualTo(deviceStateDataMock);
verify(service, never()).fetchDeviceStateDataUsingEntityDataQuery(deviceId); verify(service, never()).fetchDeviceStateDataUsingSeparateRequests(deviceId);
} }
@Test @Test
public void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() { public void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() {
service.deviceStates.clear(); service.deviceStates.clear();
willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingEntityDataQuery(deviceId); willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId);
DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId);
assertThat(deviceStateData).isEqualTo(deviceStateDataMock); assertThat(deviceStateData).isEqualTo(deviceStateDataMock);
verify(service, times(1)).fetchDeviceStateDataUsingEntityDataQuery(deviceId); verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId);
} }
@Test @Test
@ -356,7 +712,7 @@ public class DefaultDeviceStateServiceTest {
} }
private void activityVerify(boolean isActive) { private void activityVerify(boolean isActive) {
verify(telemetrySubscriptionService, times(1)).saveAttrAndNotify(any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(isActive), any()); verify(telemetrySubscriptionService).saveAttrAndNotify(any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(isActive), any());
} }
@Test @Test
@ -435,28 +791,28 @@ public class DefaultDeviceStateServiceTest {
private static Stream<Arguments> provideParametersForUpdateActivityState() { private static Stream<Arguments> provideParametersForUpdateActivityState() {
return Stream.of( return Stream.of(
Arguments.of(true, 100, 120, 80, 80, false, false), Arguments.of(true, 100, 120, 80, 80, false, false),
Arguments.of(true, 100, 120, 100, 100, false, false), Arguments.of(true, 100, 120, 100, 100, false, false),
Arguments.of(false, 100, 120, 110, 110, false, true), Arguments.of(false, 100, 120, 110, 110, false, true),
Arguments.of(true, 100, 100, 80, 80, false, false), Arguments.of(true, 100, 100, 80, 80, false, false),
Arguments.of(true, 100, 100, 100, 100, false, false), Arguments.of(true, 100, 100, 100, 100, false, false),
Arguments.of(false, 100, 100, 110, 0, true, true), Arguments.of(false, 100, 100, 110, 0, true, true),
Arguments.of(false, 100, 110, 110, 0, true, true), Arguments.of(false, 100, 110, 110, 0, true, true),
Arguments.of(false, 100, 110, 120, 0, true, true), Arguments.of(false, 100, 110, 120, 0, true, true),
Arguments.of(true, 0, 0, 0, 0, false, false), Arguments.of(true, 0, 0, 0, 0, false, false),
Arguments.of(false, 0, 0, 0, 0, true, true) Arguments.of(false, 0, 0, 0, 0, true, true)
); );
} }
@ -679,4 +1035,40 @@ public class DefaultDeviceStateServiceTest {
); );
} }
@Test
public void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() {
doAnswer(invocation -> {
Thread.sleep(100);
return deviceStateDataMock;
}).when(service).fetchDeviceStateDataUsingSeparateRequests(deviceId);
int numberOfThreads = 10;
var allThreadsReadyLatch = new CountDownLatch(numberOfThreads);
ExecutorService executor = null;
try {
executor = Executors.newFixedThreadPool(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
executor.submit(() -> {
allThreadsReadyLatch.countDown();
try {
allThreadsReadyLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
service.getOrFetchDeviceStateData(deviceId);
});
}
executor.shutdown();
await().atMost(10, TimeUnit.SECONDS).until(executor::isTerminated);
} finally {
if (executor != null) {
executor.shutdownNow();
}
}
then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId);
}
} }

266
application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java

@ -0,0 +1,266 @@
/**
* 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.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
@ExtendWith(MockitoExtension.class)
public class DefaultRuleEngineDeviceStateManagerTest {
@Mock
private static DeviceStateService deviceStateServiceMock;
@Mock
private static TbCallback tbCallbackMock;
@Mock
private static TbClusterService clusterServiceMock;
@Mock
private static TbQueueMsgMetadata metadataMock;
@Mock
private TbServiceInfoProvider serviceInfoProviderMock;
@Mock
private PartitionService partitionServiceMock;
@Captor
private static ArgumentCaptor<TbQueueCallback> queueCallbackCaptor;
private static DefaultRuleEngineDeviceStateManager deviceStateManager;
private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002"));
private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002");
private static final long EVENT_TS = System.currentTimeMillis();
private static final RuntimeException RUNTIME_EXCEPTION = new RuntimeException("Something bad happened!");
private static final TopicPartitionInfo MY_TPI = TopicPartitionInfo.builder().myPartition(true).build();
private static final TopicPartitionInfo EXTERNAL_TPI = TopicPartitionInfo.builder().myPartition(false).build();
@BeforeEach
public void setup() {
deviceStateManager = new DefaultRuleEngineDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock);
}
@ParameterizedTest
@DisplayName("Given event should be routed to local service and event processed has succeeded, " +
"when onDeviceX() is called, then should route event to local service and call onSuccess() callback.")
@MethodSource
public void givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) {
// GIVEN
given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true);
given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(MY_TPI);
onDeviceAction.run();
// THEN
actionVerification.run();
then(clusterServiceMock).shouldHaveNoInteractions();
then(tbCallbackMock).should().onSuccess();
then(tbCallbackMock).should(never()).onFailure(any());
}
private static Stream<Arguments> givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback() {
return Stream.of(
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS)
)
);
}
@ParameterizedTest
@DisplayName("Given event should be routed to local service and event processed has failed, " +
"when onDeviceX() is called, then should route event to local service and call onFailure() callback.")
@MethodSource
public void givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback(
Runnable exceptionThrowSetup, Runnable onDeviceAction, Runnable actionVerification
) {
// GIVEN
given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true);
given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(MY_TPI);
exceptionThrowSetup.run();
// WHEN
onDeviceAction.run();
// THEN
actionVerification.run();
then(clusterServiceMock).shouldHaveNoInteractions();
then(tbCallbackMock).should(never()).onSuccess();
then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION);
}
private static Stream<Arguments> givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback() {
return Stream.of(
Arguments.of(
(Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS),
(Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS),
(Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS),
(Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS)
),
Arguments.of(
(Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS),
(Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS)
)
);
}
@ParameterizedTest
@DisplayName("Given event should be routed to external service, " +
"when onDeviceX() is called, then should send correct queue message to external service with correct callback object.")
@MethodSource
public void givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback(Runnable onDeviceAction, Runnable actionVerification) {
// WHEN
ReflectionTestUtils.setField(deviceStateManager, "deviceStateService", Optional.empty());
given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(false);
given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(EXTERNAL_TPI);
onDeviceAction.run();
// THEN
actionVerification.run();
TbQueueCallback callback = queueCallbackCaptor.getValue();
callback.onSuccess(metadataMock);
then(tbCallbackMock).should().onSuccess();
callback.onFailure(RUNTIME_EXCEPTION);
then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION);
}
private static Stream<Arguments> givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback() {
return Stream.of(
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> {
var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits())
.setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits())
.setLastConnectTime(EVENT_TS)
.build();
var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setDeviceConnectMsg(deviceConnectMsg)
.build();
then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture());
}
),
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> {
var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits())
.setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits())
.setLastActivityTime(EVENT_TS)
.build();
var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setDeviceActivityMsg(deviceActivityMsg)
.build();
then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture());
}
),
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> {
var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits())
.setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits())
.setLastDisconnectTime(EVENT_TS)
.build();
var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setDeviceDisconnectMsg(deviceDisconnectMsg)
.build();
then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture());
}
),
Arguments.of(
(Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock),
(Runnable) () -> {
var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits())
.setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits())
.setLastInactivityTime(EVENT_TS)
.build();
var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setDeviceInactivityMsg(deviceInactivityMsg)
.build();
then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture());
}
)
);
}
}

4
common/cache/pom.xml

@ -40,6 +40,10 @@
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId> <artifactId>data</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>message</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId> <artifactId>spring-boot-autoconfigure</artifactId>

14
dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java → common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.dao.util.limits; package org.thingsboard.server.cache.limits;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
@ -28,25 +28,25 @@ import org.thingsboard.server.common.data.exception.TenantProfileNotFoundExcepti
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Lazy
@Service @Service
@Slf4j @Slf4j
public class DefaultRateLimitService implements RateLimitService { public class DefaultRateLimitService implements RateLimitService {
private final TbTenantProfileCache tenantProfileCache; private final TenantProfileProvider tenantProfileProvider;
private final NotificationRuleProcessor notificationRuleProcessor; private final NotificationRuleProcessor notificationRuleProcessor;
public DefaultRateLimitService(TbTenantProfileCache tenantProfileCache, public DefaultRateLimitService(TenantProfileProvider tenantProfileProvider,
@Lazy NotificationRuleProcessor notificationRuleProcessor, @Lazy NotificationRuleProcessor notificationRuleProcessor,
@Value("${cache.rateLimits.timeToLiveInMinutes:120}") int rateLimitsTtl, @Value("${cache.rateLimits.timeToLiveInMinutes:120}") int rateLimitsTtl,
@Value("${cache.rateLimits.maxSize:200000}") int rateLimitsCacheMaxSize) { @Value("${cache.rateLimits.maxSize:200000}") int rateLimitsCacheMaxSize) {
this.tenantProfileCache = tenantProfileCache; this.tenantProfileProvider = tenantProfileProvider;
this.notificationRuleProcessor = notificationRuleProcessor; this.notificationRuleProcessor = notificationRuleProcessor;
this.rateLimits = Caffeine.newBuilder() this.rateLimits = Caffeine.newBuilder()
.expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES) .expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES)
@ -66,7 +66,7 @@ public class DefaultRateLimitService implements RateLimitService {
if (tenantId.isSysTenantId()) { if (tenantId.isSysTenantId()) {
return true; return true;
} }
TenantProfile tenantProfile = tenantProfileCache.get(tenantId); TenantProfile tenantProfile = tenantProfileProvider.get(tenantId);
if (tenantProfile == null) { if (tenantProfile == null) {
throw new TenantProfileNotFoundException(tenantId); throw new TenantProfileNotFoundException(tenantId);
} }

2
dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java → common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.dao.util.limits; package org.thingsboard.server.cache.limits;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;

25
common/cache/src/main/java/org/thingsboard/server/cache/limits/TenantProfileProvider.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.cache.limits;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
public interface TenantProfileProvider {
TenantProfile get(TenantId tenantId);
}

8
common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java

@ -17,8 +17,12 @@ package org.thingsboard.server.queue;
import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.Queue;
import java.util.List;
public interface TbQueueClusterService { public interface TbQueueClusterService {
void onQueueChange(Queue queue);
void onQueueDelete(Queue queue); void onQueuesUpdate(List<Queue> queues);
void onQueuesDelete(List<Queue> queues);
} }

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

@ -91,4 +91,6 @@ public interface EdgeService extends EntityDaoService {
PageData<EdgeId> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); PageData<EdgeId> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink);
String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName); String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName);
ListenableFuture<Boolean> isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String activityState);
} }

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

@ -31,6 +31,8 @@ public enum LimitedApi {
REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false), REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false),
WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true), WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true),
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true), CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
PASSWORD_RESET(false, true), PASSWORD_RESET(false, true),
TWO_FA_VERIFICATION_CODE_SEND(false, true), TWO_FA_VERIFICATION_CODE_SEND(false, true),
TWO_FA_VERIFICATION_CODE_CHECK(false, true), TWO_FA_VERIFICATION_CODE_CHECK(false, true),

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -28,6 +28,7 @@ public enum NotificationType {
ENTITIES_LIMIT, ENTITIES_LIMIT,
API_USAGE_LIMIT, API_USAGE_LIMIT,
RULE_NODE, RULE_NODE,
RATE_LIMITS RATE_LIMITS,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE
} }

66
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.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.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeCommunicationFailureNotificationInfo implements RuleOriginatedNotificationInfo {
private TenantId tenantId;
private CustomerId customerId;
private EdgeId edgeId;
private String edgeName;
private String failureMsg;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"edgeId", edgeId.toString(),
"edgeName", edgeName,
"failureMsg", failureMsg
);
}
@Override
public TenantId getAffectedTenantId() {
return tenantId;
}
@Override
public CustomerId getAffectedCustomerId() {
return customerId;
}
@Override
public EntityId getStateEntityId() {
return edgeId;
}
}

66
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.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.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeConnectionNotificationInfo implements RuleOriginatedNotificationInfo {
private String eventType;
private TenantId tenantId;
private CustomerId customerId;
private EdgeId edgeId;
private String edgeName;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"eventType", eventType,
"edgeId", edgeId.toString(),
"edgeName", edgeName
);
}
@Override
public TenantId getAffectedTenantId() {
return tenantId;
}
@Override
public CustomerId getAffectedCustomerId() {
return customerId;
}
@Override
public EntityId getStateEntityId() {
return edgeId;
}
}

63
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java

@ -0,0 +1,63 @@
/**
* 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.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final CustomerId customerId;
private final EdgeId edgeId;
private final String edgeName;
private final String failureMsg;
private final String error;
@Override
public boolean deduplicate() {
return true;
}
@Override
public String getDeduplicationKey() {
return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), error);
}
@Override
public long getDefaultDeduplicationDuration() {
return TimeUnit.MINUTES.toMillis(30);
}
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
@Override
public EntityId getOriginatorEntityId() {
return edgeId;
}
}

62
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java

@ -0,0 +1,62 @@
/**
* 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.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class EdgeConnectionTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final CustomerId customerId;
private final EdgeId edgeId;
private final boolean connected;
private final String edgeName;
@Override
public boolean deduplicate() {
return true;
}
@Override
public String getDeduplicationKey() {
return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), String.valueOf(connected));
}
@Override
public long getDefaultDeduplicationDuration() {
return TimeUnit.MINUTES.toMillis(1);
}
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
@Override
public EntityId getOriginatorEntityId() {
return edgeId;
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.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.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeCommunicationFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> edges; // if empty - all edges
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
}

44
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java

@ -0,0 +1,44 @@
/**
* 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.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeConnectionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> edges; // if empty - all edges
private Set<EdgeConnectivityEvent> notifyOn;
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
public enum EdgeConnectivityEvent {
CONNECTED, DISCONNECTED
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java

@ -36,6 +36,8 @@ import java.io.Serializable;
@Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"), @Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"),
@Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"), @Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"),
@Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"), @Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
@Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"),
@Type(value = EdgeCommunicationFailureNotificationRuleTriggerConfig.class, name = "EDGE_COMMUNICATION_FAILURE"),
}) })
public interface NotificationRuleTriggerConfig extends Serializable { public interface NotificationRuleTriggerConfig extends Serializable {

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java

@ -26,6 +26,8 @@ public enum NotificationRuleTriggerType {
ALARM_ASSIGNMENT, ALARM_ASSIGNMENT,
DEVICE_ACTIVITY, DEVICE_ACTIVITY,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE,
NEW_PLATFORM_VERSION(false), NEW_PLATFORM_VERSION(false),
ENTITIES_LIMIT(false), ENTITIES_LIMIT(false),
API_USAGE_LIMIT(false), API_USAGE_LIMIT(false),

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

@ -81,6 +81,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String cassandraQueryTenantRateLimitsConfiguration; private String cassandraQueryTenantRateLimitsConfiguration;
private String edgeEventRateLimits;
private String edgeEventRateLimitsPerEdge;
private int defaultStorageTtlDays; private int defaultStorageTtlDays;
private int alarmsTtlDays; private int alarmsTtlDays;
private int rpcTtlDays; private int rpcTtlDays;

3
common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java

@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.Map; import java.util.Map;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.base.Strings.nullToEmpty;
@ -49,7 +50,7 @@ public class TemplateUtils {
value = FUNCTIONS.get(function).apply(value); value = FUNCTIONS.get(function).apply(value);
} }
} }
return value; return Matcher.quoteReplacement(value);
}); });
} }

12
common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java

@ -22,22 +22,22 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE;
import static org.thingsboard.server.common.data.msg.TbMsgType.NA;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE;
import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE;
import static org.thingsboard.server.common.data.msg.TbMsgType.GENERATOR_NODE_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.MSG_COUNT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.MSG_COUNT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.NA;
import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_FAILURE; import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_FAILURE;
import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_SUCCESS; import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_SUCCESS;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.GENERATOR_NODE_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL;
class TbMsgTypeTest { class TbMsgTypeTest {
private static final List<TbMsgType> typesWithNullRuleNodeConnection = List.of( private static final List<TbMsgType> typesWithNullRuleNodeConnection = List.of(
ALARM, ALARM,
ALARM_DELETE, ALARM_DELETE,

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

@ -131,6 +131,11 @@ public final class TbMsg implements Serializable {
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
} }
public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, long ts) {
return new TbMsg(null, UUID.randomUUID(), ts, type, originator, null,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
}
// REALLY NEW MSG // REALLY NEW MSG
/** /**
@ -335,7 +340,7 @@ public final class TbMsg implements Serializable {
this.originator = originator; this.originator = originator;
if (customerId == null || customerId.isNullUid()) { if (customerId == null || customerId.isNullUid()) {
if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) { if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) {
this.customerId = (CustomerId) originator; this.customerId = new CustomerId(originator.getId());
} else { } else {
this.customerId = null; this.customerId = null;
} }

39
common/proto/src/main/proto/queue.proto

@ -551,6 +551,14 @@ message GetOtaPackageResponseMsg {
string fileName = 8; string fileName = 8;
} }
message DeviceConnectProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 deviceIdMSB = 3;
int64 deviceIdLSB = 4;
int64 lastConnectTime = 5;
}
message DeviceActivityProto { message DeviceActivityProto {
int64 tenantIdMSB = 1; int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2; int64 tenantIdLSB = 2;
@ -559,6 +567,22 @@ message DeviceActivityProto {
int64 lastActivityTime = 5; int64 lastActivityTime = 5;
} }
message DeviceDisconnectProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 deviceIdMSB = 3;
int64 deviceIdLSB = 4;
int64 lastDisconnectTime = 5;
}
message DeviceInactivityProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 deviceIdMSB = 3;
int64 deviceIdLSB = 4;
int64 lastInactivityTime = 5;
}
//Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level.
message SubscriptionInfoProto { message SubscriptionInfoProto {
int64 lastActivityTime = 1; int64 lastActivityTime = 1;
@ -1271,6 +1295,9 @@ message ToCoreMsg {
LifecycleEventProto lifecycleEventMsg = 8; LifecycleEventProto lifecycleEventMsg = 8;
ErrorEventProto errorEventMsg = 9; ErrorEventProto errorEventMsg = 9;
ToDeviceActorNotificationMsgProto toDeviceActorNotification = 10; ToDeviceActorNotificationMsgProto toDeviceActorNotification = 10;
DeviceConnectProto deviceConnectMsg = 50;
DeviceDisconnectProto deviceDisconnectMsg = 51;
DeviceInactivityProto deviceInactivityMsg = 52;
} }
/* High priority messages with low latency are handled by ThingsBoard Core Service separately */ /* High priority messages with low latency are handled by ThingsBoard Core Service separately */
@ -1280,8 +1307,8 @@ message ToCoreNotificationMsg {
FromDeviceRPCResponseProto fromDeviceRpcResponse = 2; FromDeviceRPCResponseProto fromDeviceRpcResponse = 2;
bytes componentLifecycleMsg = 3 [deprecated = true]; bytes componentLifecycleMsg = 3 [deprecated = true];
bytes edgeEventUpdateMsg = 4 [deprecated = true]; bytes edgeEventUpdateMsg = 4 [deprecated = true];
QueueUpdateMsg queueUpdateMsg = 5; repeated QueueUpdateMsg queueUpdateMsgs = 5;
QueueDeleteMsg queueDeleteMsg = 6; repeated QueueDeleteMsg queueDeleteMsgs = 6;
VersionControlResponseMsg vcResponseMsg = 7; VersionControlResponseMsg vcResponseMsg = 7;
bytes toEdgeSyncRequestMsg = 8 [deprecated = true]; bytes toEdgeSyncRequestMsg = 8 [deprecated = true];
bytes fromEdgeSyncResponseMsg = 9 [deprecated = true]; bytes fromEdgeSyncResponseMsg = 9 [deprecated = true];
@ -1307,8 +1334,8 @@ message ToRuleEngineMsg {
message ToRuleEngineNotificationMsg { message ToRuleEngineNotificationMsg {
bytes componentLifecycleMsg = 1 [deprecated = true]; bytes componentLifecycleMsg = 1 [deprecated = true];
FromDeviceRPCResponseProto fromDeviceRpcResponse = 2; FromDeviceRPCResponseProto fromDeviceRpcResponse = 2;
QueueUpdateMsg queueUpdateMsg = 3; repeated QueueUpdateMsg queueUpdateMsgs = 3;
QueueDeleteMsg queueDeleteMsg = 4; repeated QueueDeleteMsg queueDeleteMsgs = 4;
ComponentLifecycleMsgProto componentLifecycle = 5; ComponentLifecycleMsgProto componentLifecycle = 5;
} }
@ -1328,8 +1355,8 @@ message ToTransportMsg {
ResourceUpdateMsg resourceUpdateMsg = 12; ResourceUpdateMsg resourceUpdateMsg = 12;
ResourceDeleteMsg resourceDeleteMsg = 13; ResourceDeleteMsg resourceDeleteMsg = 13;
UplinkNotificationMsg uplinkNotificationMsg = 14; UplinkNotificationMsg uplinkNotificationMsg = 14;
QueueUpdateMsg queueUpdateMsg = 15; repeated QueueUpdateMsg queueUpdateMsgs = 15;
QueueDeleteMsg queueDeleteMsg = 16; repeated QueueDeleteMsg queueDeleteMsgs = 16;
} }
message UsageStatsKVProto{ message UsageStatsKVProto{

47
common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java

@ -0,0 +1,47 @@
/**
* 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 org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import java.util.function.Consumer;
public class SimpleTbQueueCallback implements TbQueueCallback {
private final Consumer<TbQueueMsgMetadata> onSuccess;
private final Consumer<Throwable> onFailure;
public SimpleTbQueueCallback(Consumer<TbQueueMsgMetadata> onSuccess, Consumer<Throwable> onFailure) {
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
if (onSuccess != null) {
onSuccess.accept(metadata);
}
}
@Override
public void onFailure(Throwable t) {
if (onFailure != null) {
onFailure.accept(t);
}
}
}

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

@ -171,27 +171,35 @@ public class HashPartitionService implements PartitionService {
} }
@Override @Override
public void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { public void updateQueues(List<TransportProtos.QueueUpdateMsg> queueUpdateMsgs) {
TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); for (TransportProtos.QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); TenantId tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB()));
partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId);
partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic());
myPartitions.remove(queueKey); partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions());
if (!tenantId.isSysTenantId()) { if (!tenantId.isSysTenantId()) {
tenantRoutingInfoMap.remove(tenantId); tenantRoutingInfoMap.remove(tenantId);
}
} }
} }
@Override @Override
public void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { public void removeQueues(List<TransportProtos.QueueDeleteMsg> queueDeleteMsgs) {
TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); List<QueueKey> queueKeys = queueDeleteMsgs.stream()
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); .map(queueDeleteMsg -> {
myPartitions.remove(queueKey); TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB()));
partitionTopicsMap.remove(queueKey); return new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId);
partitionSizesMap.remove(queueKey); })
evictTenantInfo(tenantId); .collect(Collectors.toList());
queueKeys.forEach(queueKey -> {
myPartitions.remove(queueKey);
partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey);
evictTenantInfo(queueKey.getTenantId());
});
if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) {
publishPartitionChangeEvent(ServiceType.TB_RULE_ENGINE, Map.of(queueKey, Collections.emptySet())); publishPartitionChangeEvent(ServiceType.TB_RULE_ENGINE, queueKeys.stream()
.collect(Collectors.toMap(k -> k, k -> Collections.emptySet())));
} }
} }
@ -321,13 +329,11 @@ public class HashPartitionService implements PartitionService {
.forEach(removed::add); .forEach(removed::add);
} }
removed.forEach(queueKey -> { removed.forEach(queueKey -> {
log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey);
changedPartitionsMap.put(queueKey, Collections.emptySet()); changedPartitionsMap.put(queueKey, Collections.emptySet());
}); });
myPartitions.forEach((queueKey, partitions) -> { myPartitions.forEach((queueKey, partitions) -> {
if (!partitions.equals(oldPartitions.get(queueKey))) { if (!partitions.equals(oldPartitions.get(queueKey))) {
log.info("[{}] NEW PARTITIONS: {}", queueKey, partitions);
Set<TopicPartitionInfo> tpiList = partitions.stream() Set<TopicPartitionInfo> tpiList = partitions.stream()
.map(partition -> buildTopicPartitionInfo(queueKey, partition)) .map(partition -> buildTopicPartitionInfo(queueKey, partition))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -373,14 +379,11 @@ public class HashPartitionService implements PartitionService {
} }
private void publishPartitionChangeEvent(ServiceType serviceType, Map<QueueKey, Set<TopicPartitionInfo>> partitionsMap) { private void publishPartitionChangeEvent(ServiceType serviceType, Map<QueueKey, Set<TopicPartitionInfo>> partitionsMap) {
if (log.isDebugEnabled()) { log.info("Partitions changed: {}", System.lineSeparator() + partitionsMap.entrySet().stream()
log.debug("Publishing partition change event for service type " + serviceType + ":" + System.lineSeparator() + .map(entry -> "[" + entry.getKey() + "] - [" + entry.getValue().stream()
partitionsMap.entrySet().stream() .map(tpi -> tpi.getPartition().orElse(-1).toString()).sorted()
.map(entry -> entry.getKey() + " - " + entry.getValue().stream() .collect(Collectors.joining(", ")) + "]")
.map(TopicPartitionInfo::getFullTopicName).sorted() .collect(Collectors.joining(System.lineSeparator())));
.collect(Collectors.toList()))
.collect(Collectors.joining(System.lineSeparator())));
}
PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap); PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap);
try { try {
applicationEventPublisher.publishEvent(event); applicationEventPublisher.publishEvent(event);
@ -490,7 +493,7 @@ public class HashPartitionService implements PartitionService {
} }
private void logServiceInfo(TransportProtos.ServiceInfo server) { private void logServiceInfo(TransportProtos.ServiceInfo server) {
log.info("[{}] Found common server: [{}]", server.getServiceId(), server.getServiceTypesList()); log.info("[{}] Found common server: {}", server.getServiceId(), server.getServiceTypesList());
} }
private void addNode(Map<QueueKey, List<ServiceInfo>> queueServiceList, ServiceInfo instance) { private void addNode(Map<QueueKey, List<ServiceInfo>> queueServiceList, ServiceInfo instance) {

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

@ -63,9 +63,9 @@ public interface PartitionService {
int countTransportsByType(String type); int countTransportsByType(String type);
void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg); void updateQueues(List<TransportProtos.QueueUpdateMsg> queueUpdateMsgs);
void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg); void removeQueues(List<TransportProtos.QueueDeleteMsg> queueDeleteMsgs);
void removeTenant(TenantId tenantId); void removeTenant(TenantId tenantId);

15
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java

@ -220,7 +220,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
private void onUplink(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) { private void onUplink(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
PowerSavingConfiguration profileSettings = null; PowerSavingConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
if (clientProfile.isPresent()) { if (clientProfile.isPresent()) {
profileSettings = clientProfile.get().getClientSettings(); profileSettings = clientProfile.get().getClientSettings();
@ -726,7 +726,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
private boolean isDownlinkAllowed(TbCoapClientState client) { private boolean isDownlinkAllowed(TbCoapClientState client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
PowerSavingConfiguration profileSettings = null; PowerSavingConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
if (clientProfile.isPresent()) { if (clientProfile.isPresent()) {
profileSettings = clientProfile.get().getClientSettings(); profileSettings = clientProfile.get().getClientSettings();
@ -775,11 +775,12 @@ public class DefaultCoapClientContext implements CoapClientContext {
private PowerMode getPowerMode(TbCoapClientState client) { private PowerMode getPowerMode(TbCoapClientState client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
if (powerMode == null) { if (powerMode == null) {
Optional<CoapDeviceProfileTransportConfiguration> deviceProfile = getProfile(client.getProfileId()); powerMode = PowerMode.PSM;
if (deviceProfile.isPresent()) { if (client.getProfileId() != null) {
powerMode = deviceProfile.get().getClientSettings().getPowerMode(); Optional<CoapDeviceProfileTransportConfiguration> deviceProfile = getProfile(client.getProfileId());
} else { if (deviceProfile.isPresent()) {
powerMode = PowerMode.PSM; powerMode = deviceProfile.get().getClientSettings().getPowerMode();
}
} }
} }
return powerMode; return powerMode;

5
common/transport/lwm2m/pom.xml

@ -94,6 +94,11 @@
<artifactId>junit-vintage-engine</artifactId> <artifactId>junit-vintage-engine</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.awaitility</groupId> <groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId> <artifactId>awaitility</artifactId>

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java

@ -38,6 +38,7 @@ import javax.annotation.PreDestroy;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
@ -95,6 +96,7 @@ public class LwM2MTransportBootstrapService {
dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups());
dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers());
dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS);
dtlsConfig.set(DTLS_CONNECTION_ID_LENGTH, serverConfig.getDtlsConnectionIdLength());
dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); dtlsConfig.set(DTLS_ROLE, SERVER_ONLY);
setServerWithCredentials(builder, dtlsConfig); setServerWithCredentials(builder, dtlsConfig);

4
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java

@ -41,6 +41,10 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
@Value("${transport.lwm2m.dtls.retransmission_timeout:9000}") @Value("${transport.lwm2m.dtls.retransmission_timeout:9000}")
private int dtlsRetransmissionTimeout; private int dtlsRetransmissionTimeout;
@Getter
@Value("${transport.lwm2m.dtls.connection_id_length:6}")
private Integer dtlsConnectionIdLength;
@Getter @Getter
@Value("${transport.lwm2m.timeout:}") @Value("${transport.lwm2m.timeout:}")
private Long timeout; private Long timeout;

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -43,6 +43,7 @@ import javax.annotation.PreDestroy;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
@ -139,6 +140,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups()); dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups());
dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers()); dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers());
dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS); dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS);
dtlsConfig.set(DTLS_CONNECTION_ID_LENGTH, config.getDtlsConnectionIdLength());
dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); dtlsConfig.set(DTLS_ROLE, SERVER_ONLY);
/* Create credentials */ /* Create credentials */

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -411,7 +411,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
public boolean isDownlinkAllowed(LwM2mClient client) { public boolean isDownlinkAllowed(LwM2mClient client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
OtherConfiguration profileSettings = null; OtherConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
profileSettings = clientProfile.getClientLwM2mSettings(); profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode(); powerMode = profileSettings.getPowerMode();
@ -419,7 +419,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
powerMode = PowerMode.DRX; powerMode = PowerMode.DRX;
} }
} }
if (PowerMode.DRX.equals(powerMode) || otaUpdateService.isOtaDownloading(client)) { if (powerMode == null || PowerMode.DRX.equals(powerMode) || otaUpdateService.isOtaDownloading(client)) {
return true; return true;
} }
client.lock(); client.lock();
@ -460,7 +460,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
public void onUplink(LwM2mClient client) { public void onUplink(LwM2mClient client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
OtherConfiguration profileSettings = null; OtherConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
profileSettings = clientProfile.getClientLwM2mSettings(); profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode(); powerMode = profileSettings.getPowerMode();
@ -468,7 +468,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
powerMode = PowerMode.DRX; powerMode = PowerMode.DRX;
} }
} }
if (PowerMode.DRX.equals(powerMode)) { if (powerMode == null || PowerMode.DRX.equals(powerMode)) {
client.updateLastUplinkTime(); client.updateLastUplinkTime();
return; return;
} }

22
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java

@ -29,7 +29,6 @@ import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.core.util.Validate;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.redis.RedisRegistrationStore; import org.eclipse.leshan.server.redis.RedisRegistrationStore;
import org.eclipse.leshan.server.redis.serialization.IdentitySerDes;
import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; import org.eclipse.leshan.server.redis.serialization.ObservationSerDes;
import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
import org.eclipse.leshan.server.registration.Deregistration; import org.eclipse.leshan.server.registration.Deregistration;
@ -45,6 +44,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.ScanOptions;
import org.springframework.integration.redis.util.RedisLockRegistry; import org.springframework.integration.redis.util.RedisLockRegistry;
import org.thingsboard.server.transport.lwm2m.server.store.util.LwM2MIdentitySerDes;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
@ -110,12 +110,18 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec,
long lifetimeGracePeriodInSec, int cleanLimit) { long lifetimeGracePeriodInSec, int cleanLimit) {
this(connectionFactory, schedExecutor, cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit,
new RedisLockRegistry(connectionFactory, "Registration"));
}
public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec,
long lifetimeGracePeriodInSec, int cleanLimit, RedisLockRegistry lockRegistry) {
this.connectionFactory = connectionFactory; this.connectionFactory = connectionFactory;
this.schedExecutor = schedExecutor; this.schedExecutor = schedExecutor;
this.cleanPeriod = cleanPeriodInSec; this.cleanPeriod = cleanPeriodInSec;
this.cleanLimit = cleanLimit; this.cleanLimit = cleanLimit;
this.gracePeriod = lifetimeGracePeriodInSec; this.gracePeriod = lifetimeGracePeriodInSec;
this.redisLock = new RedisLockRegistry(connectionFactory, "Registration"); this.redisLock = lockRegistry;
} }
/* *************** Redis Key utility function **************** */ /* *************** Redis Key utility function **************** */
@ -173,7 +179,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) { if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) {
removeAddrIndex(connection, oldRegistration); removeAddrIndex(connection, oldRegistration);
} }
if (!oldRegistration.getIdentity().equals(registration.getIdentity())) { if (registrationsHaveDifferentIdentities(oldRegistration, registration)) {
removeIdentityIndex(connection, oldRegistration); removeIdentityIndex(connection, oldRegistration);
} }
// remove old observation // remove old observation
@ -231,7 +237,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) {
removeAddrIndex(connection, r); removeAddrIndex(connection, r);
} }
if (!r.getIdentity().equals(updatedRegistration.getIdentity())) { if (registrationsHaveDifferentIdentities(r, updatedRegistration)) {
removeIdentityIndex(connection, r); removeIdentityIndex(connection, r);
} }
@ -402,6 +408,12 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
connection.zRem(EXP_EP, registration.getEndpoint().getBytes(UTF_8)); connection.zRem(EXP_EP, registration.getEndpoint().getBytes(UTF_8));
} }
private boolean registrationsHaveDifferentIdentities(Registration first, Registration second){
var first_identity_string = LwM2MIdentitySerDes.serialize(first.getIdentity()).toString();
var second_identity_string = LwM2MIdentitySerDes.serialize(second.getIdentity()).toString();
return !first_identity_string.equals(second_identity_string);
}
private byte[] toRegIdKey(String registrationId) { private byte[] toRegIdKey(String registrationId) {
return toKey(REG_EP_REGID_IDX, registrationId); return toKey(REG_EP_REGID_IDX, registrationId);
} }
@ -411,7 +423,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto
} }
private byte[] toRegIdentityKey(Identity identity) { private byte[] toRegIdentityKey(Identity identity) {
return toKey(REG_EP_IDENTITY, IdentitySerDes.serialize(identity).toString()); return toKey(REG_EP_IDENTITY, LwM2MIdentitySerDes.serialize(identity).toString());
} }
private byte[] toEndpointKey(String endpoint) { private byte[] toEndpointKey(String endpoint) {

63
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDes.java

@ -0,0 +1,63 @@
/**
* 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.server.store.util;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import org.apache.commons.lang3.NotImplementedException;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.util.Hex;
import java.security.PublicKey;
public class LwM2MIdentitySerDes {
private static final String KEY_ADDRESS = "address";
private static final String KEY_PORT = "port";
private static final String KEY_ID = "id";
private static final String KEY_CN = "cn";
private static final String KEY_RPK = "rpk";
protected static final String KEY_LWM2MIDENTITY_TYPE = "type";
protected static final String LWM2MIDENTITY_TYPE_UNSECURE = "unsecure";
protected static final String LWM2MIDENTITY_TYPE_PSK = "psk";
protected static final String LWM2MIDENTITY_TYPE_X509 = "x509";
protected static final String LWM2MIDENTITY_TYPE_RPK = "rpk";
public static JsonObject serialize(Identity identity) {
JsonObject o = Json.object();
if (identity.isPSK()) {
o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_PSK);
o.set(KEY_ID, identity.getPskIdentity());
} else if (identity.isRPK()) {
o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_RPK);
PublicKey publicKey = identity.getRawPublicKey();
o.set(KEY_RPK, Hex.encodeHexString(publicKey.getEncoded()));
} else if (identity.isX509()) {
o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_X509);
o.set(KEY_CN, identity.getX509CommonName());
} else {
o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_UNSECURE);
o.set(KEY_ADDRESS, identity.getPeerAddress().getHostString());
o.set(KEY_PORT, identity.getPeerAddress().getPort());
}
return o;
}
public static Identity deserialize(JsonObject peer) {
throw new NotImplementedException();
}
}

105
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java

@ -0,0 +1,105 @@
/**
* 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.bootstrap;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier;
import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
@ExtendWith(MockitoExtension.class)
public class LwM2MTransportBootstrapServiceTest {
@Mock
private LwM2MTransportServerConfig serverConfig;
@Mock
private LwM2MTransportBootstrapConfig bootstrapConfig;
@Mock
private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore;
@Mock
private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore;
@Mock
private TransportService transportService;
@Mock
private TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier;
@Test
public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){
final Integer CONNECTION_ID_LENGTH = 6;
when(serverConfig.getDtlsConnectionIdLength()).thenReturn(CONNECTION_ID_LENGTH);
var lwM2MBootstrapService = createLwM2MBootstrapService();
var server = lwM2MBootstrapService.getLhBootstrapServer();
var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint");
assertThat(securedEndpoint).isNotNull();
var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config");
assertThat(config).isNotNull();
assertThat(config.getConnectionIdGenerator()).isNotNull();
assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength"))
.isEqualTo(CONNECTION_ID_LENGTH);
}
@Test
public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){
when(serverConfig.getDtlsConnectionIdLength()).thenReturn(null);
var lwM2MBootstrapService = createLwM2MBootstrapService();
var server = lwM2MBootstrapService.getLhBootstrapServer();
var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint");
assertThat(securedEndpoint).isNotNull();
var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config");
assertThat(config).isNotNull();
assertThat(config.getConnectionIdGenerator()).isNull();
}
private LwM2MTransportBootstrapService createLwM2MBootstrapService() {
setDefaultConfigVariables();
return new LwM2MTransportBootstrapService(serverConfig, bootstrapConfig, lwM2MBootstrapSecurityStore,
lwM2MInMemoryBootstrapConfigStore, transportService, certificateVerifier);
}
private void setDefaultConfigVariables(){
when(bootstrapConfig.getPort()).thenReturn(5683);
when(bootstrapConfig.getSecurePort()).thenReturn(5684);
when(serverConfig.isRecommendedCiphers()).thenReturn(false);
when(serverConfig.getDtlsRetransmissionTimeout()).thenReturn(9000);
}
}

61
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigTest.java

@ -0,0 +1,61 @@
/**
* 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.config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = LwM2MTransportServerConfig.class)
@ContextConfiguration(classes = {LwM2MTransportServerConfig.class}, loader = SpringBootContextLoader.class)
@TestPropertySource(properties = {
"transport.sessions.report_timeout=10",
"transport.lwm2m.security.recommended_ciphers=true",
"transport.lwm2m.security.recommended_supported_groups=true",
"transport.lwm2m.downlink_pool_size=10",
"transport.lwm2m.uplink_pool_size=10",
"transport.lwm2m.ota_pool_size=10",
"transport.lwm2m.clean_period_in_sec=2",
"transport.lwm2m.dtls.connection_id_length="
})
class LwM2MTransportServerConfigTest {
@MockBean(name = "lwm2mServerCredentials")
private SslCredentialsConfig credentialsConfig;
@MockBean(name = "lwm2mTrustCredentials")
private SslCredentialsConfig trustCredentialsConfig;
@Autowired
private LwM2MTransportServerConfig serverConfig;
@Test
void getDtlsConnectionIdLength_return_null_is_property_is_empty() {
// note: transport.lwm2m.dtls.connect_id_length is set in TestPropertySource
assertThat(serverConfig.getDtlsConnectionIdLength()).isNull();
}
}

109
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportServiceTest.java

@ -0,0 +1,109 @@
/**
* 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.server;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier;
import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultLwM2mTransportServiceTest {
@Mock
private LwM2mTransportContext context;
@Mock
private LwM2MTransportServerConfig config;
@Mock
private OtaPackageDataCache otaPackageDataCache;
@Mock
private LwM2mUplinkMsgHandler handler;
@Mock
private CaliforniumRegistrationStore registrationStore;
@Mock
private TbSecurityStore securityStore;
@Mock
private TbLwM2MDtlsCertificateVerifier certificateVerifier;
@Mock
private TbLwM2MAuthorizer authorizer;
@Mock
private LwM2mVersionedModelProvider modelProvider;
@Test
public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){
final Integer CONNECTION_ID_LENGTH = 6;
when(config.getDtlsConnectionIdLength()).thenReturn(CONNECTION_ID_LENGTH);
var lwm2mService = createLwM2MService();
LeshanServer server = ReflectionTestUtils.invokeMethod(lwm2mService, "getLhServer");
assertThat(server).isNotNull();
var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint");
assertThat(securedEndpoint).isNotNull();
var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config");
assertThat(config).isNotNull();
assertThat(config.getConnectionIdGenerator()).isNotNull();
assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength"))
.isEqualTo(CONNECTION_ID_LENGTH);
}
@Test
public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){
when(config.getDtlsConnectionIdLength()).thenReturn(null);
var lwm2mService = createLwM2MService();
LeshanServer server = ReflectionTestUtils.invokeMethod(lwm2mService, "getLhServer");
assertThat(server).isNotNull();
var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint");
assertThat(securedEndpoint).isNotNull();
var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config");
assertThat(config).isNotNull();
assertThat(config.getConnectionIdGenerator()).isNull();
}
private DefaultLwM2mTransportService createLwM2MService() {
setDefaultConfigVariables();
return new DefaultLwM2mTransportService(context, config, otaPackageDataCache, handler, registrationStore,
securityStore, certificateVerifier, authorizer, modelProvider);
}
private void setDefaultConfigVariables(){
when(config.getPort()).thenReturn(5683);
when(config.getSecurePort()).thenReturn(5684);
when(config.isRecommendedCiphers()).thenReturn(false);
when(config.getDtlsRetransmissionTimeout()).thenReturn(9000);
}
}

265
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStoreTest.java

@ -0,0 +1,265 @@
/**
* 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.server.store;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.test.util.ReflectionTestUtils;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.locks.Lock;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.DEFAULT_CLEAN_LIMIT;
import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.DEFAULT_CLEAN_PERIOD;
import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.DEFAULT_GRACE_PERIOD;
@ExtendWith(MockitoExtension.class)
class TbLwM2mRedisRegistrationStoreTest {
RedisConnectionFactory connectionFactory;
RedisConnection connection;
RedisLockRegistry lockRegistry;
TbLwM2mRedisRegistrationStore registrationStore;
@BeforeEach
void setUp() {
lockRegistry = mock(RedisLockRegistry.class);
lenient().when(lockRegistry.obtain(any())).thenReturn(mock(Lock.class));
connection = mock(RedisConnection.class);
//when(connection.set(any(byte[].class), any(byte[].class))).
connectionFactory = mock(RedisConnectionFactory.class);
lenient().when(connectionFactory.getConnection()).thenReturn(connection);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1,
new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", DEFAULT_CLEAN_PERIOD)));
registrationStore = new TbLwM2mRedisRegistrationStore(connectionFactory, executorService,
DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT, lockRegistry);
}
@Test
void testAddRegistrationWithNoOldRegistration() {
setOldRegistration(null);
Registration registration = buildRegistration();
assertThat(registrationStore.addRegistration(registration)).isNull();
byte[] endpoint = registration.getEndpoint().getBytes(UTF_8);
verify(connection, times(1)).set(getRegIdKey(registration), endpoint);
verify(connection, times(1)).set(getRegAddrKey(registration), endpoint);
verify(connection, times(1)).set(getRegIdentityKey(registration), endpoint);
verify(connection, times(3)).set(any(byte[].class), any(byte[].class));
verify(connection, times(0)).del(any(byte[].class));
}
@Test
void testAddRegistrationWithOldRegistrationEqualToCurrent(){
var oldRegistration = buildRegistration();
setOldRegistration(oldRegistration);
Registration registration = buildRegistration();
var deregistration = registrationStore.addRegistration(registration);
assertThat(deregistration.getRegistration()).isEqualTo(oldRegistration);
byte[] endpoint = registration.getEndpoint().getBytes(UTF_8);
verify(connection, times(1)).set(getRegIdKey(registration), endpoint);
verify(connection, times(1)).set(getRegAddrKey(registration), endpoint);
verify(connection, times(1)).set(getRegIdentityKey(registration), endpoint);
verify(connection, times(3)).set(any(byte[].class), any(byte[].class));
verify(connection, times(1)).del(getTknsRegIdKey(oldRegistration));
verify(connection, times(1)).del(any(byte[].class));
}
@Test
void testAddRegistrationRemovesIndexes(){
var oldRegistration = buildRegistration(Identity.unsecure(getTestAddress(1234)));
setOldRegistration(oldRegistration);
var registration = buildRegistration(Identity.unsecure(getTestAddress(2345)));
var deregistration = registrationStore.addRegistration(registration);
assertThat(deregistration.getRegistration()).isEqualTo(oldRegistration);
byte[] endpoint = registration.getEndpoint().getBytes(UTF_8);
verify(connection, times(1)).set(getRegIdKey(registration), endpoint);
verify(connection, times(1)).set(getRegAddrKey(registration), endpoint);
verify(connection, times(1)).set(getRegIdentityKey(registration), endpoint);
verify(connection, times(3)).set(any(byte[].class), any(byte[].class));
verify(connection, times(1)).del(getRegAddrKey(oldRegistration));
verify(connection, times(1)).del(getRegIdentityKey(oldRegistration));
verify(connection, times(1)).del(getTknsRegIdKey(oldRegistration));
verify(connection, times(3)).del(any(byte[].class));
}
@Test
void testUpdateRegistrationWhenNoRegistrationFound() {
setOldRegistration(null);
Registration registration = buildRegistration();
RegistrationUpdate update = createUpdateFromRegistration(registration);
assertThat(registrationStore.updateRegistration(update)).isNull();
verify(connection, times(1)).get(getRegIdKey(registration));
verify(connection, times(1)).get(any(byte[].class));
verify(connection, times(0)).del(any(byte[].class));
}
@Test
void testUpdateRegistrationWithSameRegistration() {
Registration registration = buildRegistration();
setOldRegistration(registration);
RegistrationUpdate update = createUpdateFromRegistration(registration);
assertThat(registrationStore.updateRegistration(update)).isNotNull();
var endpoint = registration.getEndpoint().getBytes(UTF_8);
// check registration and addressIndex here updated
verify(connection, times(1)).set(eq(getEndpointKey(endpoint)), any(byte[].class));
verify(connection, times(1)).set(getRegAddrKey(registration), endpoint);
verify(connection, times(2)).set(any(byte[].class), any(byte[].class));
verify(connection, times(0)).del(any(byte[].class));
}
@Test
void testUpdateRegistrationWithRegistrationFromSecureIdentitiesWithDifferentAddress() {
Registration oldRegistration = buildRegistration(Identity.psk(getTestAddress(1234), "my:psk"));
setOldRegistration(oldRegistration);
Registration newRegistration = buildRegistration(Identity.psk(getTestAddress(2345), "my:psk"));
RegistrationUpdate update = createUpdateFromRegistration(newRegistration);
assertThat(oldRegistration.getEndpoint()).isEqualTo(newRegistration.getEndpoint());
assertThat(registrationStore.updateRegistration(update)).isNotNull();
var endpoint = newRegistration.getEndpoint().getBytes(UTF_8);
// check registration and addressIndex here updated
verify(connection, times(1)).set(eq(getEndpointKey(endpoint)), any(byte[].class));
verify(connection, times(1)).set(getRegAddrKey(newRegistration), endpoint);
// check old AddrIndex has been removed
verify(connection, times(1)).del(getRegAddrKey(oldRegistration));
// check identityIndex has not been removed
verify(connection, times(0)).del(getRegIdentityKey(oldRegistration));
// check only one key (AddrIndex) in total was removed
verify(connection, times(1)).del(any(byte[].class));
}
@Test
void testGetRegistrationByIdentityReturnsRegistrationForSecureIdentityWithDifferentAddress() {
Registration registration = buildRegistration(Identity.psk(getTestAddress(1234), "my:psk"));
setOldRegistration(registration);
Identity sameIdentityWithDifferentAddress = Identity.psk(getTestAddress(2345), "my:psk");
Registration retrievedRegistration = registrationStore.getRegistrationByIdentity(sameIdentityWithDifferentAddress);
assertThat(retrievedRegistration).isEqualTo(registration);
}
private void setOldRegistration(Registration oldRegistration){
byte[] serializedRegistration = null;
if (oldRegistration != null){
byte[] endpoint = oldRegistration.getEndpoint().getBytes(UTF_8);
// set the AddrIndex
byte[] regAddrKey = getRegAddrKey(oldRegistration);
lenient().when(connection.get(eq(regAddrKey))).thenReturn(endpoint);
// set the IdentityIndex
byte[] regIdentityKey = getRegIdentityKey(oldRegistration);
lenient().when(connection.get(eq(regIdentityKey))).thenReturn(endpoint);
// set the IdIndex
byte[] regIdKey = getRegIdKey(oldRegistration);
lenient().when(connection.get(eq(regIdKey))).thenReturn(endpoint);
// set the registration
serializedRegistration = RegistrationSerDes.bSerialize(oldRegistration);
lenient().when(connection.get(eq(getEndpointKey(endpoint)))).thenReturn(serializedRegistration);
}
lenient().when(connection.getSet(any(byte[].class), any(byte[].class))).thenReturn(serializedRegistration);
}
private byte[] getRegAddrKey(Registration registration){
return ReflectionTestUtils.invokeMethod(registrationStore, "toRegAddrKey", registration.getSocketAddress());
}
private byte[] getRegIdentityKey(Registration registration){
return ReflectionTestUtils.invokeMethod(registrationStore, "toRegIdentityKey", registration.getIdentity());
}
private byte[] getRegIdKey(Registration registration){
return ReflectionTestUtils.invokeMethod(registrationStore, "toRegIdKey", registration.getId());
}
private byte[] getEndpointKey(byte[] endpoint){
return ReflectionTestUtils.invokeMethod(registrationStore, "toEndpointKey", endpoint);
}
private byte[] getTknsRegIdKey(Registration registration){
return ReflectionTestUtils.invokeMethod(registrationStore, "toKey", "TKNS:REGID:", registration.getId());
}
private static Registration buildRegistration() {
return buildRegistration(Identity.psk(getTestAddress(), "my:psk"));
}
private static Registration buildRegistration(Identity identity){
return new Registration.Builder("my_reg_id", "abcde", identity)
.objectLinks(new Link[]{})
.build();
}
private static RegistrationUpdate createUpdateFromRegistration(Registration registration){
return new RegistrationUpdate(
registration.getId(),
registration.getIdentity(),
registration.getLifeTimeInSec(),
registration.getSmsNumber(),
registration.getBindingMode(),
registration.getObjectLinks(),
registration.getAdditionalRegistrationAttributes()
);
}
private static InetSocketAddress getTestAddress() {
return getTestAddress(5684);
}
private static InetSocketAddress getTestAddress(int port) {
try {
return new InetSocketAddress(InetAddress.getByName("1.2.3.4"), port);
} catch (UnknownHostException e) {
throw new AssertionError("Cannot create test address");
}
}
}

77
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDesTest.java

@ -0,0 +1,77 @@
/**
* 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.server.store.util;
import com.eclipsesource.json.JsonObject;
import org.apache.commons.lang3.NotImplementedException;
import org.eclipse.leshan.core.request.Identity;
import org.junit.jupiter.api.Test;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.PublicKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class LwM2MIdentitySerDesTest {
@Test
void serializePskIdentity() {
assertThat(LwM2MIdentitySerDes.serialize(Identity.psk(getTestAddress(), "my:psk")).toString())
.isEqualTo("{\"type\":\"psk\",\"id\":\"my:psk\"}");
}
@Test
void serializeRpkIdentity() {
var public_key = mock(PublicKey.class);
when(public_key.getEncoded()).thenReturn(new byte[]{1,2,3,4,5,6,7,8,9});
assertThat(LwM2MIdentitySerDes.serialize(Identity.rpk(getTestAddress(), public_key)).toString())
.isEqualTo("{\"type\":\"rpk\",\"rpk\":\"010203040506070809\"}");
}
@Test
void serializeX509Identity() {
assertThat(LwM2MIdentitySerDes.serialize(Identity.x509(getTestAddress(), "MyCommonName")).toString())
.isEqualTo("{\"type\":\"x509\",\"cn\":\"MyCommonName\"}");
}
@Test
void serializeUnsecureIdentity() {
assertThat(LwM2MIdentitySerDes.serialize(Identity.unsecure(getTestAddress())).toString())
.isEqualTo("{\"type\":\"unsecure\",\"address\":\"1.2.3.4\",\"port\":5684}");
}
@Test
void deserialize() {
assertThatThrownBy(() -> LwM2MIdentitySerDes.deserialize(mock(JsonObject.class)))
.isInstanceOf(NotImplementedException.class);
}
private static InetSocketAddress getTestAddress() {
try {
return new InetSocketAddress(InetAddress.getByName("1.2.3.4"), 5684);
} catch (UnknownHostException e) {
throw new AssertionError("Cannot create test address");
}
}
}

2
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -152,12 +152,14 @@ public class SnmpTransportContext extends TransportContext {
try { try {
if (!newProfileTransportConfiguration.equals(sessionContext.getProfileTransportConfiguration())) { if (!newProfileTransportConfiguration.equals(sessionContext.getProfileTransportConfiguration())) {
sessionContext.setProfileTransportConfiguration(newProfileTransportConfiguration); sessionContext.setProfileTransportConfiguration(newProfileTransportConfiguration);
sessionContext.setDevice(device);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
snmpTransportService.cancelQueryingTasks(sessionContext); snmpTransportService.cancelQueryingTasks(sessionContext);
snmpTransportService.createQueryingTasks(sessionContext); snmpTransportService.createQueryingTasks(sessionContext);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) { } else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration); sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration);
sessionContext.setDevice(device);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else { } else {

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

Loading…
Cancel
Save