Browse Source

Merge pull request #10312 from thingsboard/3.6.4-sync

Merge master into develop/3.6.4
pull/10321/head
Viacheslav Klimov 3 years ago
committed by GitHub
parent
commit
2a9afc585d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/pom.xml
  2. 1
      application/src/main/data/json/system/widget_bundles/buttons.json
  3. 1
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  4. 2
      application/src/main/data/json/system/widget_types/persistent_rpc_table.json
  5. 2
      application/src/main/data/json/system/widget_types/rpc_debug_terminal.json
  6. 2
      application/src/main/data/json/system/widget_types/rpc_remote_shell.json
  7. 2
      application/src/main/data/json/system/widget_types/signal_strength.json
  8. 37
      application/src/main/data/json/system/widget_types/toggle_button.json
  9. 13
      application/src/main/data/upgrade/3.6.2/schema_update.sql
  10. 11
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  11. 12
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  12. 67
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  13. 1
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  14. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
  15. 11
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  16. 4
      application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
  17. 8
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  18. 55
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  19. 12
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  20. 2
      application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java
  21. 27
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  22. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  24. 69
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  25. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java
  26. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java
  27. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java
  28. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java
  29. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  30. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
  31. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java
  32. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/BaseAssetProfileProcessor.java
  33. 13
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java
  34. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  35. 18
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  36. 10
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  37. 129
      application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java
  38. 2
      application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
  39. 133
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java
  40. 4
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java
  41. 1
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  42. 1
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  43. 3
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  44. 13
      application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java
  45. 103
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  46. 87
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  47. 24
      application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java
  48. 12
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  49. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  50. 128
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  51. 196
      application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java
  52. 14
      application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java
  53. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
  54. 29
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  55. 2
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
  56. 8
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  57. 38
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java
  58. 6
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
  59. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
  60. 7
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java
  61. 19
      application/src/main/resources/thingsboard.yml
  62. 4
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  63. 4
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  64. 75
      application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
  65. 2
      application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java
  66. 6
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  67. 45
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  68. 137
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  69. 4
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  70. 532
      application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java
  71. 1
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  72. 420
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  73. 266
      application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java
  74. 21
      common/actor/src/main/java/org/thingsboard/server/actors/DefaultTbActorSystem.java
  75. 2
      common/actor/src/main/java/org/thingsboard/server/actors/TbActor.java
  76. 2
      common/actor/src/main/java/org/thingsboard/server/actors/TbActorCtx.java
  77. 9
      common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
  78. 2
      common/actor/src/main/java/org/thingsboard/server/actors/TbActorSystem.java
  79. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
  80. 10
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  81. 30
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  82. 10
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  83. 1
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
  84. 2
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  85. 23
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileSessionInfo.java
  86. 27
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java
  87. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java
  88. 10
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
  89. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  90. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java
  91. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  92. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
  93. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/NotificationInfo.java
  94. 35
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java
  95. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java
  96. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java
  97. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java
  98. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java
  99. 64
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java
  100. 9
      common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java

4
application/pom.xml

@ -358,6 +358,10 @@
<groupId>com.google.oauth-client</groupId> <groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId> <artifactId>google-oauth-client</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

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

@ -10,6 +10,7 @@
"widgetTypeFqns": [ "widgetTypeFqns": [
"action_button", "action_button",
"command_button", "command_button",
"toggle_button",
"power_button" "power_button"
] ]
} }

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

@ -10,6 +10,7 @@
"widgetTypeFqns": [ "widgetTypeFqns": [
"single_switch", "single_switch",
"command_button", "command_button",
"toggle_button",
"power_button", "power_button",
"slider", "slider",
"control_widgets.switch_control", "control_widgets.switch_control",

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

@ -15,7 +15,7 @@
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-persistent-table-widget-settings", "settingsDirective": "tb-persistent-table-widget-settings",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"}}"
}, },
"externalId": null, "externalId": null,
"tags": [ "tags": [

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

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>", "templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>",
"templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n",
"controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-rpc-terminal-widget-settings", "settingsDirective": "tb-rpc-terminal-widget-settings",

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

File diff suppressed because one or more lines are too long

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

@ -17,7 +17,7 @@
"settingsDirective": "tb-signal-strength-widget-settings", "settingsDirective": "tb-signal-strength-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-signal-strength-basic-config", "basicModeDirective": "tb-signal-strength-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"rssi\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (!prevValue) {\\n prevValue = Math.random() * -96;\\n}\\nvar value = prevValue + (Math.random() * 60 - 30);\\nif (value > 0) {\\n\\tvalue = 0;\\n} else if (value < -96) {\\n value = -96;\\n}\\nlet rand = Math.random();\\nreturn rand < 0.2 ? (rand < 0.1 ? -101 : '') : value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"wifi\",\"showDate\":false,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"dateColor\":\"rgba(0, 0, 0, 0.38)\",\"activeBarsColor\":{\"color\":\"rgba(92, 223, 144, 1)\",\"type\":\"range\",\"rangeList\":[{\"to\":-85,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":-85,\"to\":-70,\"color\":\"rgba(255, 122, 0, 1)\"},{\"from\":-70,\"to\":-55,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":-55,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"inactiveBarsColor\":\"rgba(224, 224, 224, 1)\",\"showTooltip\":true,\"showTooltipValue\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0,0,0,0.76)\",\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0,0,0,0.76)\",\"tooltipBackgroundColor\":\"rgba(255,255,255,0.72)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Signal strength\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dBm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"signal_cellular_alt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"rssi\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (!prevValue) {\\n prevValue = Math.random() * -96;\\n}\\nvar value = prevValue + (Math.random() * 60 - 30);\\nif (value > 0) {\\n\\tvalue = 0;\\n} else if (value < -96) {\\n value = -96;\\n}\\nlet rand = Math.random();\\nreturn rand < 0.2 ? (rand < 0.1 ? -101 : '') : value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"wifi\",\"showDate\":false,\"dateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"dateFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"dateColor\":\"rgba(0, 0, 0, 0.38)\",\"activeBarsColor\":{\"color\":\"rgba(92, 223, 144, 1)\",\"type\":\"range\",\"rangeList\":[{\"to\":-85,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":-85,\"to\":-70,\"color\":\"rgba(255, 122, 0, 1)\"},{\"from\":-70,\"to\":-55,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":-55,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"inactiveBarsColor\":\"rgba(224, 224, 224, 1)\",\"noSignalRssiValue\":-100,\"showTooltip\":true,\"showTooltipValue\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0,0,0,0.76)\",\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0,0,0,0.76)\",\"tooltipBackgroundColor\":\"rgba(255,255,255,0.72)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Signal strength\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"dBm\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"signal_cellular_alt\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
}, },
"externalId": null, "externalId": null,
"tags": [ "tags": [

37
application/src/main/data/json/system/widget_types/toggle_button.json

File diff suppressed because one or more lines are too long

13
application/src/main/data/upgrade/3.6.2/schema_update.sql

@ -28,3 +28,16 @@ ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS queue_name varchar(255);
ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS has_queue_name boolean DEFAULT false; ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS has_queue_name boolean DEFAULT false;
-- RULE NODE QUEUE UPDATE END -- RULE NODE QUEUE UPDATE END
DO
$$
BEGIN
IF NOT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'user_settings' AND column_name = 'settings' AND data_type = 'jsonb') THEN
ALTER TABLE user_settings RENAME COLUMN settings to old_settings;
ALTER TABLE user_settings ADD COLUMN settings jsonb;
UPDATE user_settings SET settings = old_settings::jsonb WHERE old_settings IS NOT NULL;
ALTER TABLE user_settings DROP COLUMN old_settings;
END IF;
END;
$$;

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

@ -31,8 +31,9 @@ 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.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
@ -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;

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

@ -17,6 +17,7 @@ package org.thingsboard.server.actors.app;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.ProcessFailureStrategy;
import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.actors.TbActorException;
@ -88,7 +89,7 @@ public class AppActor extends ContextAwareActor {
case APP_INIT_MSG: case APP_INIT_MSG:
break; break;
case PARTITION_CHANGE_MSG: case PARTITION_CHANGE_MSG:
ctx.broadcastToChildren(msg); ctx.broadcastToChildren(msg, true);
break; break;
case COMPONENT_LIFE_CYCLE_MSG: case COMPONENT_LIFE_CYCLE_MSG:
onComponentLifecycleMsg((ComponentLifecycleMsg) msg); onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
@ -202,8 +203,7 @@ public class AppActor extends ContextAwareActor {
return Optional.ofNullable(ctx.getOrCreateChildActor(new TbEntityActorId(tenantId), return Optional.ofNullable(ctx.getOrCreateChildActor(new TbEntityActorId(tenantId),
() -> DefaultActorService.TENANT_DISPATCHER_NAME, () -> DefaultActorService.TENANT_DISPATCHER_NAME,
() -> new TenantActor.ActorCreator(systemContext, tenantId), () -> new TenantActor.ActorCreator(systemContext, tenantId),
() -> systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE) || () -> true));
systemContext.getPartitionService().isManagedByCurrentService(tenantId)));
} }
private void onToEdgeSessionMsg(EdgeSessionMsg msg) { private void onToEdgeSessionMsg(EdgeSessionMsg msg) {
@ -220,6 +220,12 @@ public class AppActor extends ContextAwareActor {
} }
} }
@Override
public ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) {
log.error("Failed to process msg: {}", msg, t);
return doProcessFailure(t);
}
public static class ActorCreator extends ContextBasedCreator { public static class ActorCreator extends ContextBasedCreator {
public ActorCreator(ActorSystemContext context) { public ActorCreator(ActorSystemContext context) {

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

@ -28,13 +28,14 @@ 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;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.rule.engine.util.TenantIdLoader; import org.thingsboard.rule.engine.util.TenantIdLoader;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
@ -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);
}
}
}
} }

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

@ -161,6 +161,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
@Override @Override
public void onPartitionChangeMsg(PartitionChangeMsg msg) { public void onPartitionChangeMsg(PartitionChangeMsg msg) {
log.debug("[{}][{}] onPartitionChangeMsg: [{}]", tenantId, entityId, msg);
nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(actorRef -> actorRef.tellWithHighPriority(msg)); nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(actorRef -> actorRef.tellWithHighPriority(msg));
} }

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java

@ -50,6 +50,8 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
@Getter @Getter
protected TbActorRef rootChainActor; protected TbActorRef rootChainActor;
protected boolean ruleChainsInitialized;
public RuleChainManagerActor(ActorSystemContext systemContext, TenantId tenantId) { public RuleChainManagerActor(ActorSystemContext systemContext, TenantId tenantId) {
super(systemContext); super(systemContext);
this.tenantId = tenantId; this.tenantId = tenantId;
@ -57,6 +59,7 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
} }
protected void initRuleChains() { protected void initRuleChains() {
log.debug("[{}] Initializing rule chains", tenantId);
for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) {
RuleChainId ruleChainId = ruleChain.getId(); RuleChainId ruleChainId = ruleChain.getId();
log.debug("[{}|{}] Creating rule chain actor", ruleChainId.getEntityType(), ruleChain.getId()); log.debug("[{}|{}] Creating rule chain actor", ruleChainId.getEntityType(), ruleChain.getId());
@ -64,12 +67,15 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
visit(ruleChain, actorRef); visit(ruleChain, actorRef);
log.debug("[{}|{}] Rule Chain actor created.", ruleChainId.getEntityType(), ruleChainId.getId()); log.debug("[{}|{}] Rule Chain actor created.", ruleChainId.getEntityType(), ruleChainId.getId());
} }
ruleChainsInitialized = true;
} }
protected void destroyRuleChains() { protected void destroyRuleChains() {
log.debug("[{}] Destroying rule chains", tenantId);
for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) {
ctx.stop(new TbEntityActorId(ruleChain.getId())); ctx.stop(new TbEntityActorId(ruleChain.getId()));
} }
ruleChainsInitialized = false;
} }
protected void visit(RuleChain entity, TbActorRef actorRef) { protected void visit(RuleChain entity, TbActorRef actorRef) {

11
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.actors.ruleChain; package org.thingsboard.server.actors.ruleChain;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
@ -39,6 +40,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@Slf4j
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> { public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final String ruleChainName; private final String ruleChainName;
@ -61,6 +63,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void start(TbActorCtx context) throws Exception { public void start(TbActorCtx context) throws Exception {
if (isMyNodePartition()) { if (isMyNodePartition()) {
log.debug("[{}][{}] Starting", tenantId, entityId);
tbNode = initComponent(ruleNode); tbNode = initComponent(ruleNode);
if (tbNode != null) { if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE; state = ComponentLifecycleState.ACTIVE;
@ -95,6 +98,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void stop(TbActorCtx context) { public void stop(TbActorCtx context) {
log.debug("[{}][{}] Stopping", tenantId, entityId);
if (tbNode != null) { if (tbNode != null) {
tbNode.destroy(); tbNode.destroy();
state = ComponentLifecycleState.SUSPENDED; state = ComponentLifecycleState.SUSPENDED;
@ -103,6 +107,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception { public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception {
log.debug("[{}][{}] onPartitionChangeMsg: [{}]", tenantId, entityId, msg);
if (tbNode != null) { if (tbNode != null) {
if (!isMyNodePartition()) { if (!isMyNodePartition()) {
stop(null); stop(null);
@ -185,9 +190,13 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
private boolean isMyNodePartition(RuleNode ruleNode) { private boolean isMyNodePartition(RuleNode ruleNode) {
return ruleNode == null || !ruleNode.isSingletonMode() boolean result = ruleNode == null || !ruleNode.isSingletonMode()
|| systemContext.getDiscoveryService().isMonolith() || systemContext.getDiscoveryService().isMonolith()
|| defaultCtx.isLocalEntity(ruleNode.getId()); || defaultCtx.isLocalEntity(ruleNode.getId());
if (!result) {
log.trace("[{}][{}] Is not my node partition", tenantId, entityId);
}
return result;
} }
//Message will return after processing. See RuleChainActorMessageProcessor.pushToTarget. //Message will return after processing. See RuleChainActorMessageProcessor.pushToTarget.

4
application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java

@ -47,8 +47,8 @@ public abstract class ContextAwareActor extends AbstractTbActor {
protected abstract boolean doProcess(TbActorMsg msg); protected abstract boolean doProcess(TbActorMsg msg);
@Override @Override
public ProcessFailureStrategy onProcessFailure(Throwable t) { public ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) {
log.debug("[{}] Processing failure: ", getActorRef().getActorId(), t); log.debug("[{}] Processing failure for msg {}", getActorRef().getActorId(), msg, t);
return doProcessFailure(t); return doProcessFailure(t);
} }

8
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java

@ -31,6 +31,7 @@ import org.thingsboard.server.actors.app.AppActor;
import org.thingsboard.server.actors.app.AppInitMsg; import org.thingsboard.server.actors.app.AppInitMsg;
import org.thingsboard.server.actors.stats.StatsActor; import org.thingsboard.server.actors.stats.StatsActor;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
@ -121,7 +122,12 @@ public class DefaultActorService extends TbApplicationEventListener<PartitionCha
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent event) { protected void onTbApplicationEvent(PartitionChangeEvent event) {
log.info("Received partition change event."); log.info("Received partition change event.");
this.appActor.tellWithHighPriority(new PartitionChangeMsg(event.getServiceType())); appActor.tellWithHighPriority(new PartitionChangeMsg(event.getServiceType()));
}
@Override
protected boolean filterTbApplicationEvent(PartitionChangeEvent event) {
return event.getServiceType() == ServiceType.TB_RULE_ENGINE || event.getServiceType() == ServiceType.TB_CORE;
} }
@PreDestroy @PreDestroy

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

@ -17,6 +17,7 @@ package org.thingsboard.server.actors.tenant;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.ProcessFailureStrategy;
import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.actors.TbActorException;
@ -102,6 +103,8 @@ public class TenantActor extends RuleChainManagerActor {
log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e); log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e);
cantFindTenant = true; cantFindTenant = true;
} }
} else {
log.info("Tenant {} is not managed by current service, skipping rule chains init", tenantId);
} }
} }
log.debug("[{}] Tenant actor started.", tenantId); log.debug("[{}] Tenant actor started.", tenantId);
@ -131,20 +134,7 @@ public class TenantActor extends RuleChainManagerActor {
} }
switch (msg.getMsgType()) { switch (msg.getMsgType()) {
case PARTITION_CHANGE_MSG: case PARTITION_CHANGE_MSG:
PartitionChangeMsg partitionChangeMsg = (PartitionChangeMsg) msg; onPartitionChangeMsg((PartitionChangeMsg) msg);
ServiceType serviceType = partitionChangeMsg.getServiceType();
if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) {
//To Rule Chain Actors
broadcast(msg);
} else if (ServiceType.TB_CORE.equals(serviceType)) {
List<TbActorId> deviceActorIds = ctx.filterChildren(new TbEntityTypeActorIdPredicate(EntityType.DEVICE) {
@Override
protected boolean testEntityId(EntityId entityId) {
return super.testEntityId(entityId) && !isMyPartition(entityId);
}
});
deviceActorIds.forEach(id -> ctx.stop(id));
}
break; break;
case COMPONENT_LIFE_CYCLE_MSG: case COMPONENT_LIFE_CYCLE_MSG:
onComponentLifecycleMsg((ComponentLifecycleMsg) msg); onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
@ -239,6 +229,35 @@ public class TenantActor extends RuleChainManagerActor {
} }
} }
private void onPartitionChangeMsg(PartitionChangeMsg msg) {
ServiceType serviceType = msg.getServiceType();
if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) {
if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) {
if (!ruleChainsInitialized) {
log.info("Tenant {} is now managed by this service, initializing rule chains", tenantId);
initRuleChains();
}
} else {
if (ruleChainsInitialized) {
log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId);
destroyRuleChains();
}
return;
}
//To Rule Chain Actors
broadcast(msg);
} else if (ServiceType.TB_CORE.equals(serviceType)) {
List<TbActorId> deviceActorIds = ctx.filterChildren(new TbEntityTypeActorIdPredicate(EntityType.DEVICE) {
@Override
protected boolean testEntityId(EntityId entityId) {
return super.testEntityId(entityId) && !isMyPartition(entityId);
}
});
deviceActorIds.forEach(id -> ctx.stop(id));
}
}
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) { private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
if (msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) { if (msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
ApiUsageState old = getApiUsageState(); ApiUsageState old = getApiUsageState();
@ -266,7 +285,7 @@ public class TenantActor extends RuleChainManagerActor {
onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true);
deletedDevices.add(deviceId); deletedDevices.add(deviceId);
} }
if (isRuleEngine) { if (isRuleEngine && ruleChainsInitialized) {
TbActorRef target = getEntityActorRef(msg.getEntityId()); TbActorRef target = getEntityActorRef(msg.getEntityId());
if (target != null) { if (target != null) {
if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) {
@ -301,6 +320,12 @@ public class TenantActor extends RuleChainManagerActor {
return apiUsageState; return apiUsageState;
} }
@Override
public ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) {
log.error("[{}] Failed to process msg: {}", tenantId, msg, t);
return doProcessFailure(t);
}
public static class ActorCreator extends ContextBasedCreator { public static class ActorCreator extends ContextBasedCreator {
private final TenantId tenantId; private final TenantId tenantId;

12
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -32,8 +32,6 @@ import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -52,13 +50,12 @@ 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.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -74,8 +71,8 @@ import org.thingsboard.server.common.data.sync.vc.VcUtils;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
@ -93,7 +90,6 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@ -113,9 +109,7 @@ public class AdminController extends BaseController {
private final SmsService smsService; private final SmsService smsService;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
@Lazy
private final JwtSettingsService jwtSettingsService; private final JwtSettingsService jwtSettingsService;
@Lazy
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final EntitiesVersionControlService versionControlService; private final EntitiesVersionControlService versionControlService;
private final TbAutoCommitSettingsService autoCommitSettingsService; private final TbAutoCommitSettingsService autoCommitSettingsService;

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

@ -29,7 +29,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.NotificationTemplateId;

27
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -25,11 +25,14 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -49,6 +52,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
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.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -117,6 +121,7 @@ public class UserController extends BaseController {
public static final String PATHS = "paths"; public static final String PATHS = "paths";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s"; public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s";
public static final String MOBILE_TOKEN_HEADER = "X-Mobile-Token";
@Value("${security.user_token_access_enabled}") @Value("${security.user_token_access_enabled}")
private boolean userTokenAccessEnabled; private boolean userTokenAccessEnabled;
@ -584,6 +589,28 @@ public class UserController extends BaseController {
return userSettingsService.reportUserDashboardAction(currentUser.getTenantId(), currentUser.getId(), dashboardId, action); return userSettingsService.reportUserDashboardAction(currentUser.getTenantId(), currentUser.getId(), dashboardId, action);
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping("/user/mobile/session")
public MobileSessionInfo getMobileSession(@RequestHeader(MOBILE_TOKEN_HEADER) String mobileToken,
@AuthenticationPrincipal SecurityUser user) {
return userService.findMobileSession(user.getTenantId(), user.getId(), mobileToken);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/user/mobile/session")
public void saveMobileSession(@RequestBody MobileSessionInfo sessionInfo,
@RequestHeader(MOBILE_TOKEN_HEADER) String mobileToken,
@AuthenticationPrincipal SecurityUser user) {
userService.saveMobileSession(user.getTenantId(), user.getId(), mobileToken, sessionInfo);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping("/user/mobile/session")
public void removeMobileSession(@RequestHeader(MOBILE_TOKEN_HEADER) String mobileToken,
@AuthenticationPrincipal SecurityUser user) {
userService.removeMobileSession(user.getTenantId(), mobileToken);
}
private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException {
if (type.isReserved()) { if (type.isReserved()) {
throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);

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

@ -19,6 +19,7 @@ import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired; 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.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
@ -150,6 +151,9 @@ public class EdgeContextComponent {
@Autowired @Autowired
private ResourceService resourceService; private ResourceService resourceService;
@Autowired
private RateLimitService rateLimitService;
@Autowired @Autowired
private NotificationRuleProcessor notificationRuleProcessor; private NotificationRuleProcessor notificationRuleProcessor;

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

@ -107,6 +107,8 @@ public class EdgeEventSourcingListener {
private EdgeEventActionType getEdgeEventActionTypeForEntityEvent(Object entity) { private EdgeEventActionType getEdgeEventActionTypeForEntityEvent(Object entity) {
if (entity instanceof AlarmComment) { if (entity instanceof AlarmComment) {
return EdgeEventActionType.DELETED_COMMENT; return EdgeEventActionType.DELETED_COMMENT;
} else if (entity instanceof Alarm) {
return EdgeEventActionType.ALARM_DELETE;
} }
return EdgeEventActionType.DELETED; return EdgeEventActionType.DELETED;
} }

69
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.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; 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;
@ -109,6 +110,7 @@ public final class EdgeGrpcSession implements Closeable {
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private static final String RATE_LIMIT_REACHED = "Rate limit reached";
private final UUID sessionId; private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener; private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
@ -264,36 +266,51 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void onUplinkMsg(UplinkMsg uplinkMsg) { private void onUplinkMsg(UplinkMsg uplinkMsg) {
if (isRateLimitViolated(uplinkMsg)) {
return;
}
ListenableFuture<List<Void>> future = processUplinkMsg(uplinkMsg); ListenableFuture<List<Void>> future = processUplinkMsg(uplinkMsg);
Futures.addCallback(future, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable List<Void> result) { public void onSuccess(@Nullable List<Void> result) {
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder() sendResponseMessage(uplinkMsg.getUplinkMsgId(), true, null);
.setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(true).build();
sendDownlinkMsg(ResponseMsg.newBuilder()
.setUplinkResponseMsg(uplinkResponseMsg)
.build());
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t); String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t);
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder() sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg);
.setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(false).setErrorMsg(errorMsg).build();
sendDownlinkMsg(ResponseMsg.newBuilder()
.setUplinkResponseMsg(uplinkResponseMsg)
.build());
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} }
private boolean isRateLimitViolated(UplinkMsg uplinkMsg) {
if (!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES, tenantId) ||
!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, tenantId, edge.getId())) {
String errorMsg = String.format("Failed to process uplink message. %s", RATE_LIMIT_REACHED);
sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg);
return true;
}
return false;
}
private void sendResponseMessage(int uplinkMsgId, boolean success, String errorMsg) {
UplinkResponseMsg.Builder responseBuilder = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsgId)
.setSuccess(success);
if (errorMsg != null) {
responseBuilder.setErrorMsg(errorMsg);
}
sendDownlinkMsg(ResponseMsg.newBuilder()
.setUplinkResponseMsg(responseBuilder.build())
.build());
}
private void onDownlinkResponse(DownlinkResponseMsg msg) { private void onDownlinkResponse(DownlinkResponseMsg msg) {
try { try {
if (msg.getSuccess()) { if (msg.getSuccess()) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
log.debug("[{}][{}] Msg has been processed successfully!Msg Id: [{}], Msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg); log.debug("[{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg);
} else { } else {
log.error("[{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg()); log.error("[{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg());
} }
@ -540,6 +557,7 @@ public final class EdgeGrpcSession implements Closeable {
case UNASSIGNED_FROM_EDGE: case UNASSIGNED_FROM_EDGE:
case ALARM_ACK: case ALARM_ACK:
case ALARM_CLEAR: case ALARM_CLEAR:
case ALARM_DELETE:
case CREDENTIALS_UPDATED: case CREDENTIALS_UPDATED:
case RELATION_ADD_OR_UPDATE: case RELATION_ADD_OR_UPDATE:
case RELATION_DELETED: case RELATION_DELETED:
@ -695,11 +713,6 @@ public final class EdgeGrpcSession implements Closeable {
private ListenableFuture<List<Void>> processUplinkMsg(UplinkMsg uplinkMsg) { private ListenableFuture<List<Void>> processUplinkMsg(UplinkMsg uplinkMsg) {
List<ListenableFuture<Void>> result = new ArrayList<>(); List<ListenableFuture<Void>> result = new ArrayList<>();
try { try {
if (uplinkMsg.getEntityDataCount() > 0) {
for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) {
result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData));
}
}
if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) { if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) {
for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) { for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) {
result.add(((DeviceProfileProcessor) ctx.getDeviceProfileEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion)) result.add(((DeviceProfileProcessor) ctx.getDeviceProfileEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
@ -730,6 +743,17 @@ public final class EdgeGrpcSession implements Closeable {
.processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); .processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg));
} }
} }
if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) {
for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) {
result.add(((EntityViewProcessor) ctx.getEntityViewProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
.processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg));
}
}
if (uplinkMsg.getEntityDataCount() > 0) {
for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) {
result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData));
}
}
if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { if (uplinkMsg.getAlarmUpdateMsgCount() > 0) {
for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) {
result.add(((AlarmProcessor) ctx.getAlarmEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion)) result.add(((AlarmProcessor) ctx.getAlarmEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
@ -742,12 +766,6 @@ public final class EdgeGrpcSession implements Closeable {
.processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg)); .processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg));
} }
} }
if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) {
for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) {
result.add(((EntityViewProcessor) ctx.getEntityViewProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
.processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg));
}
}
if (uplinkMsg.getRelationUpdateMsgCount() > 0) { if (uplinkMsg.getRelationUpdateMsgCount() > 0) {
for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) {
result.add(((RelationProcessor) ctx.getRelationEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion)) result.add(((RelationProcessor) ctx.getRelationEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
@ -762,7 +780,8 @@ public final class EdgeGrpcSession implements Closeable {
} }
if (uplinkMsg.getResourceUpdateMsgCount() > 0) { if (uplinkMsg.getResourceUpdateMsgCount() > 0) {
for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) { for (ResourceUpdateMsg resourceUpdateMsg : uplinkMsg.getResourceUpdateMsgList()) {
result.add(((ResourceProcessor) ctx.getResourceEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion)).processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg)); result.add(((ResourceProcessor) ctx.getResourceEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
.processResourceMsgFromEdge(edge.getTenantId(), edge, resourceUpdateMsg));
} }
} }
if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) {

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java

@ -63,6 +63,7 @@ public class AssetMsgConstructorV1 extends BaseAssetMsgConstructor {
@Override @Override
public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) { public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) {
assetProfile = JacksonUtil.clone(assetProfile);
imageService.inlineImageForEdge(assetProfile); imageService.inlineImageForEdge(assetProfile);
AssetProfileUpdateMsg.Builder builder = AssetProfileUpdateMsg.newBuilder() AssetProfileUpdateMsg.Builder builder = AssetProfileUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
@ -89,4 +90,5 @@ public class AssetMsgConstructorV1 extends BaseAssetMsgConstructor {
} }
return builder.build(); return builder.build();
} }
} }

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java

@ -33,6 +33,7 @@ public class DashboardMsgConstructorV1 extends BaseDashboardMsgConstructor {
@Override @Override
public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) { public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) {
dashboard = JacksonUtil.clone(dashboard);
imageService.inlineImagesForEdge(dashboard); imageService.inlineImagesForEdge(dashboard);
DashboardUpdateMsg.Builder builder = DashboardUpdateMsg.newBuilder() DashboardUpdateMsg.Builder builder = DashboardUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
@ -52,4 +53,5 @@ public class DashboardMsgConstructorV1 extends BaseDashboardMsgConstructor {
} }
return builder.build(); return builder.build();
} }
} }

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java

@ -95,6 +95,7 @@ public class DeviceMsgConstructorV1 extends BaseDeviceMsgConstructor {
@Override @Override
public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) {
deviceProfile = JacksonUtil.clone(deviceProfile);
imageService.inlineImageForEdge(deviceProfile); imageService.inlineImageForEdge(deviceProfile);
DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder() DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
@ -140,4 +141,5 @@ public class DeviceMsgConstructorV1 extends BaseDeviceMsgConstructor {
} }
return builder.build(); return builder.build();
} }
} }

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java

@ -43,6 +43,7 @@ public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor {
@Override @Override
public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List<String> widgets) { public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List<String> widgets) {
widgetsBundle = JacksonUtil.clone(widgetsBundle);
imageService.inlineImageForEdge(widgetsBundle); imageService.inlineImageForEdge(widgetsBundle);
WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder() WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
@ -68,6 +69,7 @@ public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor {
@Override @Override
public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) { public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) {
widgetTypeDetails = JacksonUtil.clone(widgetTypeDetails);
imageService.inlineImagesForEdge(widgetTypeDetails); imageService.inlineImagesForEdge(widgetTypeDetails);
WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder() WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
@ -109,4 +111,5 @@ public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor {
} }
return builder.build(); return builder.build();
} }
} }

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

@ -460,6 +460,7 @@ public abstract class BaseEdgeProcessor {
case UNASSIGNED_FROM_EDGE: case UNASSIGNED_FROM_EDGE:
case RELATION_DELETED: case RELATION_DELETED:
case DELETED_COMMENT: case DELETED_COMMENT:
case ALARM_DELETE:
return UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; return UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE;
case ALARM_ACK: case ALARM_ACK:
return UpdateMsgType.ALARM_ACK_RPC_MESSAGE; return UpdateMsgType.ALARM_ACK_RPC_MESSAGE;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java

@ -105,7 +105,7 @@ public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements A
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
if (EdgeEventActionType.DELETED.equals(actionType)) { if (EdgeEventActionType.DELETED.equals(actionType) || EdgeEventActionType.ALARM_DELETE.equals(actionType)) {
Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class); Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class);
if (deletedAlarm == null) { if (deletedAlarm == null) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java

@ -154,6 +154,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
.constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm)); .constructAlarmUpdatedMsg(msgType, alarm, findOriginatorEntityName(tenantId, alarm));
} }
break; break;
case ALARM_DELETE:
case DELETED: case DELETED:
Alarm deletedAlarm = JacksonUtil.convertValue(body, Alarm.class); Alarm deletedAlarm = JacksonUtil.convertValue(body, Alarm.class);
if (deletedAlarm != null) { if (deletedAlarm != null) {

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/BaseAssetProfileProcessor.java

@ -47,7 +47,6 @@ public abstract class BaseAssetProfileProcessor extends BaseEdgeProcessor {
assetProfile.setId(assetProfileId); assetProfile.setId(assetProfileId);
assetProfile.setDefault(assetProfileById.isDefault()); assetProfile.setDefault(assetProfileById.isDefault());
} }
assetProfile.setDefault(false);
String assetProfileName = assetProfile.getName(); String assetProfileName = assetProfile.getName();
AssetProfile assetProfileByName = assetProfileService.findAssetProfileByName(tenantId, assetProfileName); AssetProfile assetProfileByName = assetProfileService.findAssetProfileByName(tenantId, assetProfileName);
if (assetProfileByName != null && !assetProfileByName.getId().equals(assetProfileId)) { if (assetProfileByName != null && !assetProfileByName.getId().equals(assetProfileId)) {

13
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java

@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@ -63,13 +62,6 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
device.setId(deviceId); device.setId(deviceId);
} }
Device savedDevice = deviceService.saveDevice(device, false); Device savedDevice = deviceService.saveDevice(device, false);
if (created) {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId()));
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
deviceCredentials.setCredentialsId(StringUtils.randomAlphanumeric(20));
deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials);
}
tbClusterService.onDeviceUpdated(savedDevice, created ? null : device); tbClusterService.onDeviceUpdated(savedDevice, created ? null : device);
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to process device update msg [{}]", tenantId, deviceUpdateMsg, e); log.error("[{}] Failed to process device update msg [{}]", tenantId, deviceUpdateMsg, e);
@ -91,6 +83,10 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
tenantId, device.getName(), deviceCredentials.getCredentialsId(), deviceCredentials.getCredentialsValue()); tenantId, device.getName(), deviceCredentials.getCredentialsId(), deviceCredentials.getCredentialsValue());
try { try {
DeviceCredentials deviceCredentialsByDeviceId = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); DeviceCredentials deviceCredentialsByDeviceId = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId());
if (deviceCredentialsByDeviceId == null) {
deviceCredentialsByDeviceId = new DeviceCredentials();
deviceCredentialsByDeviceId.setDeviceId(device.getId());
}
deviceCredentialsByDeviceId.setCredentialsType(deviceCredentials.getCredentialsType()); deviceCredentialsByDeviceId.setCredentialsType(deviceCredentials.getCredentialsType());
deviceCredentialsByDeviceId.setCredentialsId(deviceCredentials.getCredentialsId()); deviceCredentialsByDeviceId.setCredentialsId(deviceCredentials.getCredentialsId());
deviceCredentialsByDeviceId.setCredentialsValue(deviceCredentials.getCredentialsValue()); deviceCredentialsByDeviceId.setCredentialsValue(deviceCredentials.getCredentialsValue());
@ -111,4 +107,5 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg); protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Device device, DeviceUpdateMsg deviceUpdateMsg);
protected abstract DeviceCredentials constructDeviceCredentialsFromUpdateMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg); protected abstract DeviceCredentials constructDeviceCredentialsFromUpdateMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg);
} }

2
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -196,7 +196,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
public Boolean delete(Alarm alarm, User user) { public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId(); TenantId tenantId = alarm.getTenantId();
notificationEntityService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(), notificationEntityService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(),
ActionType.DELETED, user); ActionType.ALARM_DELETE, user);
return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId());
} }

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

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -62,7 +63,6 @@ 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.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;
@ -154,6 +154,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
} }
} }
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);
NotificationSettings systemSettings = tenantId.isSysTenantId() ? settings : notificationSettingsService.findNotificationSettings(TenantId.SYS_TENANT_ID);
log.debug("Processing notification request (tenantId: {}, targets: {})", tenantId, request.getTargets()); log.debug("Processing notification request (tenantId: {}, targets: {})", tenantId, request.getTargets());
request.setStatus(NotificationRequestStatus.PROCESSING); request.setStatus(NotificationRequestStatus.PROCESSING);
@ -165,6 +166,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.deliveryMethods(deliveryMethods) .deliveryMethods(deliveryMethods)
.template(notificationTemplate) .template(notificationTemplate)
.settings(settings) .settings(settings)
.systemSettings(systemSettings)
.build(); .build();
processNotificationRequestAsync(ctx, targets, callback); processNotificationRequestAsync(ctx, targets, callback);
@ -202,6 +204,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
private void processNotificationRequestAsync(NotificationProcessingContext ctx, List<NotificationTarget> targets, FutureCallback<NotificationRequestStats> callback) { private void processNotificationRequestAsync(NotificationProcessingContext ctx, List<NotificationTarget> targets, FutureCallback<NotificationRequestStats> callback) {
notificationExecutor.submit(() -> { notificationExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
NotificationRequestId requestId = ctx.getRequest().getId(); NotificationRequestId requestId = ctx.getRequest().getId();
for (NotificationTarget target : targets) { for (NotificationTarget target : targets) {
try { try {
@ -217,9 +220,16 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return; return;
} }
} }
log.debug("[{}] Notification request processing is finished", requestId);
NotificationRequestStats stats = ctx.getStats(); NotificationRequestStats stats = ctx.getStats();
long time = System.currentTimeMillis() - startTs;
int sent = stats.getTotalSent().get();
int errors = stats.getTotalErrors().get();
if (errors > 0) {
log.info("[{}][{}] Notification request processing finished in {} ms (sent: {}, errors: {})", ctx.getTenantId(), requestId, time, sent, errors);
} else {
log.info("[{}][{}] Notification request processing finished in {} ms (sent: {})", ctx.getTenantId(), requestId, time, sent);
}
updateRequestStats(ctx, requestId, stats); updateRequestStats(ctx, requestId, stats);
if (callback != null) { if (callback != null) {
callback.onSuccess(stats); callback.onSuccess(stats);
@ -243,11 +253,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
if (targetConfig.getUsersFilter().getType().isForRules() && ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) { if (targetConfig.getUsersFilter().getType().isForRules() && ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) {
recipients = new PageDataIterable<>(pageLink -> { recipients = new PageDataIterable<>(pageLink -> {
return notificationTargetService.findRecipientsForRuleNotificationTargetConfig(ctx.getTenantId(), targetConfig, (RuleOriginatedNotificationInfo) ctx.getRequest().getInfo(), pageLink); return notificationTargetService.findRecipientsForRuleNotificationTargetConfig(ctx.getTenantId(), targetConfig, (RuleOriginatedNotificationInfo) ctx.getRequest().getInfo(), pageLink);
}, 500); }, 256);
} else { } else {
recipients = new PageDataIterable<>(pageLink -> { recipients = new PageDataIterable<>(pageLink -> {
return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink); return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink);
}, 500); }, 256);
} }
break; break;
} }

10
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -43,6 +43,7 @@ public class NotificationProcessingContext {
@Getter @Getter
private final TenantId tenantId; private final TenantId tenantId;
private final NotificationSettings settings; private final NotificationSettings settings;
private final NotificationSettings systemSettings;
@Getter @Getter
private final NotificationRequest request; private final NotificationRequest request;
@Getter @Getter
@ -58,11 +59,12 @@ public class NotificationProcessingContext {
@Builder @Builder
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, Set<NotificationDeliveryMethod> deliveryMethods, public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, Set<NotificationDeliveryMethod> deliveryMethods,
NotificationTemplate template, NotificationSettings settings) { NotificationTemplate template, NotificationSettings settings, NotificationSettings systemSettings) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.request = request; this.request = request;
this.deliveryMethods = deliveryMethods; this.deliveryMethods = deliveryMethods;
this.settings = settings; this.settings = settings;
this.systemSettings = systemSettings;
this.notificationTemplate = template; this.notificationTemplate = template;
this.notificationType = template.getNotificationType(); this.notificationType = template.getNotificationType();
this.templates = new EnumMap<>(NotificationDeliveryMethod.class); this.templates = new EnumMap<>(NotificationDeliveryMethod.class);
@ -81,6 +83,12 @@ public class NotificationProcessingContext {
} }
public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) { public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) {
NotificationSettings settings;
if (deliveryMethod == NotificationDeliveryMethod.MOBILE_APP) {
settings = this.systemSettings;
} else {
settings = this.settings;
}
return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod); return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod);
} }

129
application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java

@ -0,0 +1,129 @@
/**
* 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.channels;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Strings;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@Component
@RequiredArgsConstructor
@Slf4j
public class MobileAppNotificationChannel implements NotificationChannel<User, MobileAppDeliveryMethodNotificationTemplate> {
private final FirebaseService firebaseService;
private final UserService userService;
private final NotificationSettingsService notificationSettingsService;
@Override
public void sendNotification(User recipient, MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
var mobileSessions = userService.findMobileSessions(recipient.getTenantId(), recipient.getId());
if (mobileSessions.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app");
}
MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE_APP);
String credentials = config.getFirebaseServiceAccountCredentials();
Set<String> validTokens = new HashSet<>(mobileSessions.keySet());
String subject = processedTemplate.getSubject();
String body = processedTemplate.getBody();
Map<String, String> data = getNotificationData(processedTemplate, ctx);
for (String token : mobileSessions.keySet()) {
try {
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data);
} catch (FirebaseMessagingException e) {
MessagingErrorCode errorCode = e.getMessagingErrorCode();
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) {
validTokens.remove(token);
userService.removeMobileSession(recipient.getTenantId(), token);
continue;
}
throw new RuntimeException("Failed to send message via FCM: " + e.getMessage(), e);
}
}
if (validTokens.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app");
}
}
private Map<String, String> getNotificationData(MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
Map<String, String> data = Optional.ofNullable(processedTemplate.getAdditionalConfig())
.filter(JsonNode::isObject).map(JacksonUtil::toFlatMap).orElseGet(HashMap::new);
NotificationInfo info = ctx.getRequest().getInfo();
if (info == null) {
return data;
}
Optional.ofNullable(info.getStateEntityId()).ifPresent(stateEntityId -> {
data.put("stateEntityId", stateEntityId.getId().toString());
data.put("stateEntityType", stateEntityId.getEntityType().name());
if (!"true".equals(data.get("onClick.enabled")) && info.getDashboardId() != null) {
data.put("onClick.enabled", "true");
data.put("onClick.linkType", "DASHBOARD");
data.put("onClick.setEntityIdInState", "true");
data.put("onClick.dashboardId", info.getDashboardId().toString());
}
});
data.put("notificationType", ctx.getNotificationType().name());
switch (ctx.getNotificationType()) {
case ALARM:
case ALARM_ASSIGNMENT:
case ALARM_COMMENT:
info.getTemplateData().forEach((key, value) -> {
data.put("info." + key, value);
});
break;
}
data.replaceAll((key, value) -> Strings.nullToEmpty(value));
return data;
}
@Override
public void check(TenantId tenantId) throws Exception {
NotificationSettings systemSettings = notificationSettingsService.findNotificationSettings(TenantId.SYS_TENANT_ID);
if (!systemSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.MOBILE_APP)) {
throw new RuntimeException("Push-notifications to mobile are not configured");
}
}
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.MOBILE_APP;
}
}

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

@ -17,7 +17,7 @@ package org.thingsboard.server.service.notification.channels;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;

133
application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java

@ -0,0 +1,133 @@
/**
* 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.provider;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class DefaultFirebaseService implements FirebaseService {
private final Cache<String, FirebaseContext> contexts = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.<String, FirebaseContext>removalListener((key, context, cause) -> {
if (cause == RemovalCause.EXPIRED && context != null) {
context.destroy();
}
})
.build();
@Override
public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map<String, String> data) throws FirebaseMessagingException {
FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> {
if (context == null) {
return new FirebaseContext(key, credentials);
} else {
context.check(credentials);
return context;
}
});
Message message = Message.builder()
.setToken(fcmToken)
.setNotification(Notification.builder()
.setTitle(title)
.setBody(body)
.build())
.setAndroidConfig(AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.build())
.putAllData(data)
.build();
firebaseContext.getMessaging().send(message);
log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken);
}
public static class FirebaseContext {
private final String key;
private String credentials;
private FirebaseApp app;
@Getter
private FirebaseMessaging messaging;
public FirebaseContext(String key, String credentials) {
this.key = key;
this.credentials = credentials;
init();
}
private void init() {
FirebaseOptions options;
try {
options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(IOUtils.toInputStream(credentials, StandardCharsets.UTF_8)))
.build();
} catch (IOException e) {
throw new RuntimeException("Failed to process service account credentials: " + e.getMessage(), e);
}
try {
app = FirebaseApp.initializeApp(options, key);
} catch (IllegalStateException alreadyExists) { // should never normally happen
app = FirebaseApp.getInstance(key);
}
try {
messaging = FirebaseMessaging.getInstance(app);
} catch (IllegalStateException alreadyExists) { // should never normally happen
messaging = FirebaseMessaging.getInstance(app);
}
log.debug("[{}] Initialized new FirebaseContext", key);
}
public void check(String credentials) {
if (!this.credentials.equals(credentials)) {
destroy();
this.credentials = credentials;
init();
} else if (app == null || messaging == null) {
throw new IllegalStateException("Firebase app couldn't be initialized");
}
}
public void destroy() {
app.delete();
app = null;
messaging = null;
log.debug("[{}] Destroyed FirebaseContext", key);
}
}
}

4
application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java → application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.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.service.slack; package org.thingsboard.server.service.notification.provider;
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;
@ -29,7 +29,7 @@ import com.slack.api.methods.response.users.UsersListResponse;
import com.slack.api.model.ConversationType; import com.slack.api.model.ConversationType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;

1
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -65,6 +65,7 @@ public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerP
.alarmSeverity(alarmInfo.getSeverity()) .alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus()) .alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId()) .alarmCustomerId(alarmInfo.getCustomerId())
.dashboardId(alarmInfo.getDashboardId())
.build(); .build();
} }

1
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -76,6 +76,7 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc
.alarmSeverity(alarm.getSeverity()) .alarmSeverity(alarm.getSeverity())
.alarmStatus(alarm.getStatus()) .alarmStatus(alarm.getStatus())
.alarmCustomerId(alarm.getCustomerId()) .alarmCustomerId(alarm.getCustomerId())
.dashboardId(alarm.getDashboardId())
.build(); .build();
} }

3
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -22,11 +22,11 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule;
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.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@ -111,6 +111,7 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
.acknowledged(alarmInfo.isAcknowledged()) .acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared()) .cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId()) .alarmCustomerId(alarmInfo.getCustomerId())
.dashboardId(alarmInfo.getDashboardId())
.build(); .build();
} }

13
application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java

@ -89,11 +89,14 @@ public abstract class AbstractPartitionBasedService<T extends EntityId> extends
*/ */
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (getServiceType().equals(partitionChangeEvent.getServiceType())) { log.debug("onTbApplicationEvent, processing event: {}", partitionChangeEvent);
log.debug("onTbApplicationEvent, processing event: {}", partitionChangeEvent); subscribeQueue.add(partitionChangeEvent.getPartitions());
subscribeQueue.add(partitionChangeEvent.getPartitions()); scheduledExecutor.submit(this::pollInitStateFromDB);
scheduledExecutor.submit(this::pollInitStateFromDB); }
}
@Override
protected boolean filterTbApplicationEvent(PartitionChangeEvent event) {
return getServiceType().equals(event.getServiceType());
} }
protected void pollInitStateFromDB() { protected void pollInitStateFromDB() {

103
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,
@ -167,7 +171,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
GitVersionControlQueueService vcQueueService, GitVersionControlQueueService vcQueueService,
PartitionService partitionService, PartitionService partitionService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
Optional<JwtSettingsService> jwtSettingsService, JwtSettingsService jwtSettingsService,
NotificationSchedulerService notificationSchedulerService, NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor, NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService) { TbImageService imageService) {
@ -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)
@ -220,16 +228,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent event) { protected void onTbApplicationEvent(PartitionChangeEvent event) {
if (event.getServiceType().equals(getServiceType())) { log.info("Subscribing to partitions: {}", event.getPartitions());
log.info("Subscribing to partitions: {}", event.getPartitions()); this.mainConsumer.subscribe(event.getPartitions());
this.mainConsumer.subscribe(event.getPartitions()); this.usageStatsConsumer.subscribe(
this.usageStatsConsumer.subscribe( event
event .getPartitions()
.getPartitions() .stream()
.stream() .map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic()))
.map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic())) .collect(Collectors.toSet()));
.collect(Collectors.toSet()));
}
this.firmwareStatesConsumer.subscribe(); this.firmwareStatesConsumer.subscribe();
} }
@ -265,14 +271,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) {
@ -641,25 +656,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) {

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

@ -54,6 +54,7 @@ import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineConsumerContext; import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineConsumerContext;
import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineQueueConsumerManager; import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineQueueConsumerManager;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.ArrayList; import java.util.ArrayList;
@ -85,9 +86,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, ApplicationEventPublisher eventPublisher) { PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty()); eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), jwtSettingsService);
this.ctx = ctx; this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService; this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService; this.queueService = queueService;
@ -99,27 +102,32 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
List<Queue> queues = queueService.findAllQueues(); List<Queue> queues = queueService.findAllQueues();
for (Queue configuration : queues) { for (Queue configuration : queues) {
if (partitionService.isManagedByCurrentService(configuration.getTenantId())) { if (partitionService.isManagedByCurrentService(configuration.getTenantId())) {
initConsumer(configuration); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, configuration);
createConsumer(queueKey, configuration);
} }
} }
} }
private void initConsumer(Queue configuration) {
getOrCreateConsumer(new QueueKey(ServiceType.TB_RULE_ENGINE, configuration)).init(configuration);
}
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent event) { protected void onTbApplicationEvent(PartitionChangeEvent event) {
if (event.getServiceType().equals(getServiceType())) { event.getPartitionsMap().forEach((queueKey, partitions) -> {
event.getPartitionsMap().forEach((queueKey, partitions) -> { if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) {
var consumer = consumers.get(queueKey); var consumer = getConsumer(queueKey).orElseGet(() -> {
if (consumer != null) { Queue config = queueService.findQueueByTenantIdAndName(queueKey.getTenantId(), queueKey.getQueueName());
consumer.update(partitions); return createConsumer(queueKey, config);
} else { });
log.warn("Received invalid partition change event for {} that is not managed by this service", queueKey); consumer.update(partitions);
} }
}); });
} consumers.keySet().stream()
.collect(Collectors.groupingBy(QueueKey::getTenantId))
.forEach((tenantId, queueKeys) -> {
if (!partitionService.isManagedByCurrentService(tenantId)) {
queueKeys.forEach(queueKey -> {
removeConsumer(queueKey).ifPresent(TbRuleEngineQueueConsumerManager::stop);
});
}
});
} }
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
@ -179,7 +187,6 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
} }
private void updateQueues(List<QueueUpdateMsg> queueUpdateMsgs) { private void updateQueues(List<QueueUpdateMsg> queueUpdateMsgs) {
boolean partitionsChanged = false;
for (QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { for (QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) {
log.info("Received queue update msg: [{}]", queueUpdateMsg); log.info("Received queue update msg: [{}]", queueUpdateMsg);
TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB()));
@ -189,23 +196,14 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId);
Queue queue = queueService.findQueueById(tenantId, queueId); Queue queue = queueService.findQueueById(tenantId, queueId);
TbRuleEngineQueueConsumerManager consumerManager = getOrCreateConsumer(queueKey); getConsumer(queueKey).ifPresentOrElse(consumer -> consumer.update(queue),
Queue oldQueue = consumerManager.getQueue(); () -> createConsumer(queueKey, queue));
consumerManager.update(queue);
if (oldQueue == null || queue.getPartitions() != oldQueue.getPartitions()) {
partitionsChanged = true;
}
} else {
partitionsChanged = true;
} }
} }
if (partitionsChanged) { partitionService.updateQueues(queueUpdateMsgs);
partitionService.updateQueues(queueUpdateMsgs); partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(),
partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE)));
new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE)));
}
} }
private void deleteQueues(List<QueueDeleteMsg> queueDeleteMsgs) { private void deleteQueues(List<QueueDeleteMsg> queueDeleteMsgs) {
@ -213,10 +211,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
log.info("Received queue delete msg: [{}]", queueDeleteMsg); log.info("Received queue delete msg: [{}]", queueDeleteMsg);
TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB()));
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId);
var consumerManager = consumers.remove(queueKey); removeConsumer(queueKey).ifPresent(consumer -> consumer.delete(true));
if (consumerManager != null) {
consumerManager.delete(true);
}
} }
partitionService.removeQueues(queueDeleteMsgs); partitionService.removeQueues(queueDeleteMsgs);
@ -231,17 +226,25 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
.filter(queueKey -> queueKey.getTenantId().equals(event.getTenantId())) .filter(queueKey -> queueKey.getTenantId().equals(event.getTenantId()))
.collect(Collectors.toList()); .collect(Collectors.toList());
toRemove.forEach(queueKey -> { toRemove.forEach(queueKey -> {
var consumerManager = consumers.remove(queueKey); removeConsumer(queueKey).ifPresent(consumer -> consumer.delete(false));
if (consumerManager != null) {
consumerManager.delete(false);
}
}); });
} }
} }
} }
private TbRuleEngineQueueConsumerManager getOrCreateConsumer(QueueKey queueKey) { private Optional<TbRuleEngineQueueConsumerManager> getConsumer(QueueKey queueKey) {
return consumers.computeIfAbsent(queueKey, key -> new TbRuleEngineQueueConsumerManager(ctx, key)); return Optional.ofNullable(consumers.get(queueKey));
}
private TbRuleEngineQueueConsumerManager createConsumer(QueueKey queueKey, Queue queue) {
var consumer = new TbRuleEngineQueueConsumerManager(ctx, queueKey);
consumers.put(queueKey, consumer);
consumer.init(queue);
return consumer;
}
private Optional<TbRuleEngineQueueConsumerManager> removeConsumer(QueueKey queueKey) {
return Optional.ofNullable(consumers.remove(queueKey));
} }
@Scheduled(fixedDelayString = "${queue.rule-engine.stats.print-interval-ms}") @Scheduled(fixedDelayString = "${queue.rule-engine.stats.print-interval-ms}")

24
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();

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

@ -49,7 +49,6 @@ import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsServ
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -75,14 +74,14 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final ApplicationEventPublisher eventPublisher; protected final ApplicationEventPublisher eventPublisher;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer; protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final Optional<JwtSettingsService> jwtSettingsService; protected final JwtSettingsService jwtSettingsService;
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache, TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService, TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, ApplicationEventPublisher eventPublisher, PartitionService partitionService, ApplicationEventPublisher eventPublisher,
TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) { TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, JwtSettingsService jwtSettingsService) {
this.actorContext = actorContext; this.actorContext = actorContext;
this.encodingService = encodingService; this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache; this.tenantProfileCache = tenantProfileCache;
@ -108,6 +107,11 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
launchMainConsumers(); launchMainConsumers();
} }
@Override
protected boolean filterTbApplicationEvent(PartitionChangeEvent event) {
return event.getServiceType() == getServiceType();
}
protected abstract ServiceType getServiceType(); protected abstract ServiceType getServiceType();
protected abstract void launchMainConsumers(); protected abstract void launchMainConsumers();
@ -176,7 +180,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
} }
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) { } else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) { if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings); jwtSettingsService.reloadJwtSettings();
return; return;
} else { } else {
tenantProfileCache.evict(tenantId); tenantProfileCache.evict(tenantId);

5
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java

@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
@ -42,9 +41,7 @@ import java.util.Optional;
@Slf4j @Slf4j
public class DefaultJwtSettingsService implements JwtSettingsService { public class DefaultJwtSettingsService implements JwtSettingsService {
@Lazy
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
@Lazy
private final Optional<TbClusterService> tbClusterService; private final Optional<TbClusterService> tbClusterService;
private final Optional<NotificationCenter> notificationCenter; private final Optional<NotificationCenter> notificationCenter;
private final JwtSettingsValidator jwtSettingsValidator; private final JwtSettingsValidator jwtSettingsValidator;
@ -107,11 +104,13 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
@Override @Override
public JwtSettings reloadJwtSettings() { public JwtSettings reloadJwtSettings() {
log.trace("Executing reloadJwtSettings");
return getJwtSettings(true); return getJwtSettings(true);
} }
@Override @Override
public JwtSettings getJwtSettings() { public JwtSettings getJwtSettings() {
log.trace("Executing getJwtSettings");
return getJwtSettings(false); return getJwtSettings(false);
} }

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;

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

@ -259,13 +259,14 @@ 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); // We substitute the subscriptionId with cmdId for old-style subscriptions.
doSendUpdate(sessionId, cmdId, update.copyWithNewSubscriptionId(cmdId));
} }
@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 +275,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 +289,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 +440,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 +450,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 +546,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 +555,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 +644,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 +699,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 +837,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,

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.ws.telemetry.sub; package org.thingsboard.server.service.ws.telemetry.sub;
import lombok.AllArgsConstructor;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
@ -26,8 +27,8 @@ import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@AllArgsConstructor
public class TelemetrySubscriptionUpdate { public class TelemetrySubscriptionUpdate {
private final int subscriptionId; private final int subscriptionId;
private int errorCode; private int errorCode;
private String errorMsg; private String errorMsg;
@ -93,6 +94,10 @@ public class TelemetrySubscriptionUpdate {
return errorMsg; return errorMsg;
} }
public TelemetrySubscriptionUpdate copyWithNewSubscriptionId(int subscriptionId){
return new TelemetrySubscriptionUpdate(subscriptionId, errorCode, errorMsg, data);
}
@Override @Override
public String toString() { public String toString() {
StringBuilder result = new StringBuilder("TelemetrySubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="); StringBuilder result = new StringBuilder("TelemetrySubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=");

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

@ -309,7 +309,7 @@ cassandra:
# Interval in milliseconds for printing Cassandra query queue statistic # Interval in milliseconds for printing Cassandra query queue statistic
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data type values except target to null for the same ts on save # set all data type values except target to null for the same ts on save
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}"
# log one of cassandra queries with specified frequency (0 - logging is disabled) # log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
tenant_rate_limits: tenant_rate_limits:
@ -379,7 +379,9 @@ sql:
# Enable/disable TTL (Time To Live) for timeseries records # Enable/disable TTL (Time To Live) for timeseries records
enabled: "${SQL_TTL_TS_ENABLED:true}" enabled: "${SQL_TTL_TS_ENABLED:true}"
execution_interval_ms: "${SQL_TTL_TS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day execution_interval_ms: "${SQL_TTL_TS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day
ts_key_value_ttl: "${SQL_TTL_TS_TS_KEY_VALUE_TTL:0}" # Number of seconds # The parameter to specify system TTL(Time To Live) value for timeseries records. Value set in seconds.
# 0 - records are never expired.
ts_key_value_ttl: "${SQL_TTL_TS_TS_KEY_VALUE_TTL:0}"
events: events:
# Enable/disable TTL (Time To Live) for event records # Enable/disable TTL (Time To Live) for event records
enabled: "${SQL_TTL_EVENTS_ENABLED:true}" enabled: "${SQL_TTL_EVENTS_ENABLED:true}"
@ -786,7 +788,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 +796,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 +1055,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}"

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

@ -189,6 +189,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected String token; protected String token;
protected String refreshToken; protected String refreshToken;
protected String mobileToken;
protected String username; protected String username;
protected TenantId tenantId; protected TenantId tenantId;
@ -573,6 +574,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
if (this.token != null) { if (this.token != null) {
request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
} }
if (this.mobileToken != null) {
request.header(UserController.MOBILE_TOKEN_HEADER, this.mobileToken);
}
} }
protected DeviceProfile createDeviceProfile(String name) { protected DeviceProfile createDeviceProfile(String name) {

4
application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java

@ -261,7 +261,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_DELETE);
} }
@Test @Test
@ -274,7 +274,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_DELETE);
} }
@Test @Test

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

@ -63,7 +63,6 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -73,9 +72,9 @@ public class HashPartitionServiceTest {
public static final int ITERATIONS = 1000000; public static final int ITERATIONS = 1000000;
public static final int SERVER_COUNT = 3; public static final int SERVER_COUNT = 3;
private HashPartitionService clusterRoutingService; private HashPartitionService partitionService;
private TbServiceInfoProvider discoveryService; private TbServiceInfoProvider serviceInfoProvider;
private TenantRoutingInfoService routingInfoService; private TenantRoutingInfoService routingInfoService;
private ApplicationEventPublisher applicationEventPublisher; private ApplicationEventPublisher applicationEventPublisher;
private QueueRoutingInfoService queueRoutingInfoService; private QueueRoutingInfoService queueRoutingInfoService;
@ -85,19 +84,17 @@ public class HashPartitionServiceTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
discoveryService = mock(TbServiceInfoProvider.class); serviceInfoProvider = mock(TbServiceInfoProvider.class);
applicationEventPublisher = mock(ApplicationEventPublisher.class); applicationEventPublisher = mock(ApplicationEventPublisher.class);
routingInfoService = mock(TenantRoutingInfoService.class); routingInfoService = mock(TenantRoutingInfoService.class);
queueRoutingInfoService = mock(QueueRoutingInfoService.class); queueRoutingInfoService = mock(QueueRoutingInfoService.class);
topicService = mock(TopicService.class); topicService = mock(TopicService.class);
when(topicService.buildTopicName(Mockito.any())).thenAnswer(i -> i.getArguments()[0]); when(topicService.buildTopicName(Mockito.any())).thenAnswer(i -> i.getArguments()[0]);
clusterRoutingService = createPartitionService(); partitionService = createPartitionService();
ServiceInfo currentServer = ServiceInfo.newBuilder() ServiceInfo currentServer = ServiceInfo.newBuilder()
.setServiceId("tb-core-0") .setServiceId("tb-core-0")
.addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name())) .addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name()))
.build(); .build();
// when(queueService.resolve(Mockito.any(), Mockito.anyString())).thenAnswer(i -> i.getArguments()[1]);
// when(discoveryService.getServiceInfo()).thenReturn(currentServer);
List<ServiceInfo> otherServers = new ArrayList<>(); List<ServiceInfo> otherServers = new ArrayList<>();
for (int i = 1; i < SERVER_COUNT; i++) { for (int i = 1; i < SERVER_COUNT; i++) {
otherServers.add(ServiceInfo.newBuilder() otherServers.add(ServiceInfo.newBuilder()
@ -106,7 +103,7 @@ public class HashPartitionServiceTest {
.build()); .build());
} }
clusterRoutingService.recalculatePartitions(currentServer, otherServers); partitionService.recalculatePartitions(currentServer, otherServers);
} }
@Test @Test
@ -122,7 +119,7 @@ public class HashPartitionServiceTest {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Map<Integer, Integer> map = new HashMap<>(); Map<Integer, Integer> map = new HashMap<>();
for (DeviceId deviceId : devices) { for (DeviceId deviceId : devices) {
TopicPartitionInfo address = clusterRoutingService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, deviceId); TopicPartitionInfo address = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, deviceId);
Integer partition = address.getPartition().get(); Integer partition = address.getPartition().get();
map.put(partition, map.getOrDefault(partition, 0) + 1); map.put(partition, map.getOrDefault(partition, 0) + 1);
} }
@ -156,7 +153,7 @@ public class HashPartitionServiceTest {
for (int queueIndex = 0; queueIndex < queueCount; queueIndex++) { for (int queueIndex = 0; queueIndex < queueCount; queueIndex++) {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, "queue" + queueIndex, tenantId);
for (int partition = 0; partition < partitionCount; partition++) { for (int partition = 0; partition < partitionCount; partition++) {
ServiceInfo serviceInfo = clusterRoutingService.resolveByPartitionIdx(services, queueKey, partition); ServiceInfo serviceInfo = partitionService.resolveByPartitionIdx(services, queueKey, partition, Collections.emptyMap());
String serviceId = serviceInfo.getServiceId(); String serviceId = serviceInfo.getServiceId();
map.put(serviceId, map.get(serviceId) + 1); map.put(serviceId, map.get(serviceId) + 1);
} }
@ -233,15 +230,34 @@ public class HashPartitionServiceTest {
} }
Map<QueueKey, Map<ServiceInfo, List<Integer>>> serversPartitions = new HashMap<>(); Map<QueueKey, Map<ServiceInfo, List<Integer>>> serversPartitions = new HashMap<>();
clusterRoutingService.init(); when(serviceInfoProvider.isService(eq(ServiceType.TB_RULE_ENGINE))).thenReturn(true);
partitionService.init();
for (ServiceInfo ruleEngine : ruleEngines) { for (ServiceInfo ruleEngine : ruleEngines) {
List<ServiceInfo> other = new ArrayList<>(ruleEngines); List<ServiceInfo> other = new ArrayList<>(ruleEngines);
other.removeIf(serviceInfo -> serviceInfo.getServiceId().equals(ruleEngine.getServiceId())); other.removeIf(serviceInfo -> serviceInfo.getServiceId().equals(ruleEngine.getServiceId()));
clusterRoutingService.recalculatePartitions(ruleEngine, other); partitionService.recalculatePartitions(ruleEngine, other);
clusterRoutingService.myPartitions.forEach((queueKey, partitions) -> { partitionService.myPartitions.forEach((queueKey, partitions) -> {
serversPartitions.computeIfAbsent(queueKey, k -> new HashMap<>()).put(ruleEngine, partitions); serversPartitions.computeIfAbsent(queueKey, k -> new HashMap<>()).put(ruleEngine, partitions);
}); });
Set<UUID> assignedTenantProfiles = ruleEngine.getAssignedTenantProfilesList().stream().map(UUID::fromString).collect(Collectors.toSet());
when(serviceInfoProvider.getAssignedTenantProfiles()).thenReturn(assignedTenantProfiles);
if (assignedTenantProfiles.isEmpty()) {
assertThat(partitionService.isManagedByCurrentService(TenantId.SYS_TENANT_ID)).isTrue();
tenants.forEach((tenantId, tenantProfileId) -> {
assertThat(partitionService.isManagedByCurrentService(tenantId)).isFalse();
});
} else {
assertThat(partitionService.isManagedByCurrentService(TenantId.SYS_TENANT_ID)).isFalse();
tenants.forEach((tenantId, tenantProfileId) -> {
if (assignedTenantProfiles.contains(tenantProfileId.getId())) {
assertThat(partitionService.isManagedByCurrentService(tenantId)).isTrue();
} else {
assertThat(partitionService.isManagedByCurrentService(tenantId)).isFalse();
}
});
}
} }
assertThat(serversPartitions.keySet()).containsAll(queues.stream().map(queue -> new QueueKey(ServiceType.TB_RULE_ENGINE, queue)).collect(Collectors.toList())); assertThat(serversPartitions.keySet()).containsAll(queues.stream().map(queue -> new QueueKey(ServiceType.TB_RULE_ENGINE, queue)).collect(Collectors.toList()));
@ -286,7 +302,7 @@ public class HashPartitionServiceTest {
mockRoutingInfo(tenantId, tenantProfileId, false); // not isolated yet mockRoutingInfo(tenantId, tenantProfileId, false); // not isolated yet
mockQueues(queues); mockQueues(queues);
when(discoveryService.isService(eq(ServiceType.TB_RULE_ENGINE))).thenReturn(true); when(serviceInfoProvider.isService(eq(ServiceType.TB_RULE_ENGINE))).thenReturn(true);
Mockito.reset(applicationEventPublisher); Mockito.reset(applicationEventPublisher);
HashPartitionService partitionService_common = createPartitionService(); HashPartitionService partitionService_common = createPartitionService();
partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine)); partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine));
@ -349,27 +365,6 @@ public class HashPartitionServiceTest {
}); });
} }
@Test
public void testIsManagedByCurrentServiceCheck() {
TenantProfileId isolatedProfileId = new TenantProfileId(UUID.randomUUID());
when(discoveryService.getAssignedTenantProfiles()).thenReturn(Set.of(isolatedProfileId.getId())); // dedicated server
TenantProfileId regularProfileId = new TenantProfileId(UUID.randomUUID());
TenantId isolatedTenantId = new TenantId(UUID.randomUUID());
mockRoutingInfo(isolatedTenantId, isolatedProfileId, true);
TenantId regularTenantId = new TenantId(UUID.randomUUID());
mockRoutingInfo(regularTenantId, regularProfileId, false);
assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue();
assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isFalse();
when(discoveryService.getAssignedTenantProfiles()).thenReturn(Collections.emptySet()); // common server
assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue();
assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isTrue();
}
@Test @Test
public void testPartitionsDistribution_sameTenantDifferentQueues() { public void testPartitionsDistribution_sameTenantDifferentQueues() {
List<ServiceInfo> ruleEngines = new ArrayList<>(); List<ServiceInfo> ruleEngines = new ArrayList<>();
@ -389,9 +384,9 @@ public class HashPartitionServiceTest {
.limit(100).collect(Collectors.toList()); .limit(100).collect(Collectors.toList());
for (int partition = 0; partition < 10; partition++) { for (int partition = 0; partition < 10; partition++) {
ServiceInfo expectedAssignedRuleEngine = clusterRoutingService.resolveByPartitionIdx(ruleEngines, new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId), partition); ServiceInfo expectedAssignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId), partition, Collections.emptyMap());
for (QueueKey queueKey : queues) { for (QueueKey queueKey : queues) {
ServiceInfo assignedRuleEngine = clusterRoutingService.resolveByPartitionIdx(ruleEngines, queueKey, partition); ServiceInfo assignedRuleEngine = partitionService.resolveByPartitionIdx(ruleEngines, queueKey, partition, Collections.emptyMap());
assertThat(assignedRuleEngine).as(queueKey + "[" + partition + "] should be assigned to " + expectedAssignedRuleEngine.getServiceId()) assertThat(assignedRuleEngine).as(queueKey + "[" + partition + "] should be assigned to " + expectedAssignedRuleEngine.getServiceId())
.isEqualTo(expectedAssignedRuleEngine); .isEqualTo(expectedAssignedRuleEngine);
} }
@ -403,9 +398,9 @@ public class HashPartitionServiceTest {
verify(applicationEventPublisher).publishEvent(argThat(event -> event instanceof PartitionChangeEvent && predicate.test((PartitionChangeEvent) event))); verify(applicationEventPublisher).publishEvent(argThat(event -> event instanceof PartitionChangeEvent && predicate.test((PartitionChangeEvent) event)));
} }
private void mockRoutingInfo(TenantId tenantId, TenantProfileId tenantProfileId, boolean isolatedTbRuleEngine) { private void mockRoutingInfo(TenantId tenantId, TenantProfileId tenantProfileId, boolean isolated) {
when(routingInfoService.getRoutingInfo(eq(tenantId))) when(routingInfoService.getRoutingInfo(eq(tenantId)))
.thenReturn(new TenantRoutingInfo(tenantId, tenantProfileId, isolatedTbRuleEngine)); .thenReturn(new TenantRoutingInfo(tenantId, tenantProfileId, isolated));
} }
private void mockQueues(List<Queue> queues) { private void mockQueues(List<Queue> queues) {
@ -424,7 +419,7 @@ public class HashPartitionServiceTest {
} }
private HashPartitionService createPartitionService() { private HashPartitionService createPartitionService() {
HashPartitionService partitionService = new HashPartitionService(discoveryService, HashPartitionService partitionService = new HashPartitionService(serviceInfoProvider,
routingInfoService, routingInfoService,
applicationEventPublisher, applicationEventPublisher,
queueRoutingInfoService, queueRoutingInfoService,

2
application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java

@ -129,7 +129,7 @@ public class DefaultTbAlarmServiceTest {
public void testDelete() { public void testDelete() {
service.delete(new Alarm(), new User()); service.delete(new Alarm(), new User());
verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.DELETED), any()); verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_DELETE), any());
verify(alarmSubscriptionService, times(1)).deleteAlarm(any(), any()); verify(alarmSubscriptionService, times(1)).deleteAlarm(any(), any());
} }

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

@ -71,6 +71,8 @@ public class RateLimitServiceTest {
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit);
profileConfiguration.setEdgeEventRateLimits(rateLimit); profileConfiguration.setEdgeEventRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit); profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit);
profileConfiguration.setEdgeUplinkMessagesRateLimits(rateLimit);
profileConfiguration.setEdgeUplinkMessagesRateLimitsPerEdge(rateLimit);
updateTenantProfileConfiguration(profileConfiguration); updateTenantProfileConfiguration(profileConfiguration);
for (LimitedApi limitedApi : List.of( for (LimitedApi limitedApi : List.of(
@ -80,7 +82,9 @@ public class RateLimitServiceTest {
LimitedApi.REST_REQUESTS_PER_CUSTOMER, LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES, LimitedApi.CASSANDRA_QUERIES,
LimitedApi.EDGE_EVENTS, LimitedApi.EDGE_EVENTS,
LimitedApi.EDGE_EVENTS_PER_EDGE LimitedApi.EDGE_EVENTS_PER_EDGE,
LimitedApi.EDGE_UPLINK_MESSAGES,
LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE
)) { )) {
testRateLimits(limitedApi, max, tenantId); testRateLimits(limitedApi, max, tenantId);
} }

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

@ -22,7 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.util.Pair; import org.springframework.data.util.Pair;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR
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.NotificationRuleInfo; import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
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.settings.NotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
@ -48,6 +49,8 @@ import org.thingsboard.server.common.data.notification.targets.platform.UserList
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.HasSubject;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
@ -57,8 +60,10 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
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.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationRuleService;
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.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
@ -92,7 +97,11 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
@Autowired @Autowired
protected NotificationRequestService notificationRequestService; protected NotificationRequestService notificationRequestService;
@Autowired @Autowired
protected NotificationSettingsService notificationSettingsService;
@Autowired
protected SqlPartitioningRepository partitioningRepository; protected SqlPartitioningRepository partitioningRepository;
@Autowired
protected DefaultNotifications defaultNotifications;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@ -104,6 +113,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID); notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID); notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1); partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1);
notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID);
} }
protected NotificationTarget createNotificationTarget(UserId... usersIds) { protected NotificationTarget createNotificationTarget(UserId... usersIds) {
@ -168,26 +178,28 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate;
switch (deliveryMethod) { switch (deliveryMethod) {
case WEB: { case WEB: {
WebDeliveryMethodNotificationTemplate template = new WebDeliveryMethodNotificationTemplate(); deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate();
template.setSubject(subject);
deliveryMethodNotificationTemplate = template;
break; break;
} }
case EMAIL: { case EMAIL: {
EmailDeliveryMethodNotificationTemplate template = new EmailDeliveryMethodNotificationTemplate(); deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
template.setSubject(subject);
deliveryMethodNotificationTemplate = template;
break; break;
} }
case SMS: { case SMS: {
deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate();
break; break;
} }
case MOBILE_APP:
deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate();
break;
default: default:
throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod);
} }
deliveryMethodNotificationTemplate.setEnabled(true); deliveryMethodNotificationTemplate.setEnabled(true);
deliveryMethodNotificationTemplate.setBody(text); deliveryMethodNotificationTemplate.setBody(text);
if (deliveryMethodNotificationTemplate instanceof HasSubject) {
((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject);
}
config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate);
} }
notificationTemplate.setConfiguration(config); notificationTemplate.setConfiguration(config);
@ -202,6 +214,15 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
doPost("/api/notification/settings", notificationSettings).andExpect(status().isOk()); doPost("/api/notification/settings", notificationSettings).andExpect(status().isOk());
} }
protected void saveNotificationSettings(NotificationDeliveryMethodConfig... configs) throws Exception {
NotificationSettings settings = new NotificationSettings();
settings.setDeliveryMethodsConfigs(Arrays.stream(configs)
.collect(Collectors.toMap(
NotificationDeliveryMethodConfig::getMethod, config -> config
)));
saveNotificationSettings(settings);
}
protected Pair<User, NotificationApiWsClient> createUserAndConnectWsClient(Authority authority) throws Exception { protected Pair<User, NotificationApiWsClient> createUserAndConnectWsClient(Authority authority) throws Exception {
User user = new User(); User user = new User();
user.setTenantId(tenantId); user.setTenantId(tenantId);
@ -231,10 +252,14 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
} }
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) { protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB); return createNotificationRule(triggerConfig, subject, text, List.of(targets), NotificationDeliveryMethod.WEB);
}
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, List<NotificationTargetId> targets, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, deliveryMethods);
NotificationRule rule = new NotificationRule(); NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " " + Arrays.toString(targets)); rule.setName(triggerConfig.getTriggerType() + " " + targets);
rule.setEnabled(true); rule.setEnabled(true);
rule.setTemplateId(template.getId()); rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType()); rule.setTriggerType(triggerConfig.getTriggerType());
@ -242,7 +267,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType()); recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(List.of(targets))); recipientsConfig.setTargets(DaoUtil.toUUIDs(targets));
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);
return saveNotificationRule(rule); return saveNotificationRule(rule);

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

@ -24,16 +24,25 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
@ -44,11 +53,14 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
@ -60,6 +72,7 @@ import org.thingsboard.server.common.data.notification.template.DeliveryMethodNo
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate.Button.LinkType; import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate.Button.LinkType;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
@ -70,7 +83,6 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.notification.NotificationDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
@ -86,11 +98,16 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@Slf4j @Slf4j
@ -101,9 +118,9 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationDao notificationDao; private NotificationDao notificationDao;
@Autowired @Autowired
private DbCallbackExecutorService executor;
@Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@MockBean
private FirebaseService firebaseService;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
@ -708,6 +725,120 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams); assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams);
} }
@Test
public void testMobileAppNotifications() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginCustomerUser();
mobileToken = "customerFcmToken";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
loginTenantAdmin();
mobileToken = "tenantFcmToken1";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
mobileToken = "tenantFcmToken2";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
loginDifferentCustomer(); // with no mobile info
loginTenantAdmin();
NotificationTarget target = createNotificationTarget(new AllUsersFilter());
NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP);
((MobileAppDeliveryMethodNotificationTemplate) template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.MOBILE_APP))
.setAdditionalConfig(JacksonUtil.newObjectNode().set("test", JacksonUtil.newObjectNode().put("test", "test")));
saveNotificationTemplate(template);
NotificationRequest request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0);
NotificationRequestStats stats = awaitNotificationRequest(request.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.MOBILE_APP)).hasValue(2);
assertThat(stats.getErrors().get(NotificationDeliveryMethod.MOBILE_APP).get(differentCustomerUser.getEmail()))
.contains("doesn't use the mobile app");
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken1"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))));
verifyNoMoreInteractions(firebaseService);
clearInvocations(firebaseService);
doDelete("/api/user/mobile/session").andExpect(status().isOk());
request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0);
awaitNotificationRequest(request.getId());
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken1"), eq("Title"), eq("Message"), anyMap());
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap());
verifyNoMoreInteractions(firebaseService);
}
@Test
public void testMobileAppNotifications_ruleBased() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginTenantAdmin();
mobileToken = "tenantFcmToken";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(),
DefaultNotifications.alarmComment.getSubject(), DefaultNotifications.alarmComment.getText(),
List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP);
Device device = createDevice("test", "test");
UUID alarmDashboardId = UUID.randomUUID();
Alarm alarm = Alarm.builder()
.type("test")
.tenantId(tenantId)
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.details(JacksonUtil.newObjectNode()
.put("dashboardId", alarmDashboardId.toString()))
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
AlarmComment comment = new AlarmComment();
comment.setComment(JacksonUtil.newObjectNode()
.put("text", "text"));
doPost("/api/alarm/" + alarm.getId() + "/comment", comment, AlarmComment.class);
ArgumentCaptor<Map<String, String>> msgCaptor = ArgumentCaptor.forClass(Map.class);
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken"), eq("Comment on 'test' alarm"),
eq(TENANT_ADMIN_EMAIL + " added comment: text"),
msgCaptor.capture());
});
Map<String, String> firebaseMessageData = msgCaptor.getValue();
assertThat(firebaseMessageData.keySet()).doesNotContainNull().doesNotContain("");
assertThat(firebaseMessageData.values()).doesNotContainNull();
assertThat(firebaseMessageData.get("info.userEmail")).isEqualTo(TENANT_ADMIN_EMAIL);
assertThat(firebaseMessageData.get("info.alarmType")).isEqualTo("test");
assertThat(firebaseMessageData.get("onClick.enabled")).isEqualTo("true");
assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD");
assertThat(firebaseMessageData.get("onClick.dashboardId")).isEqualTo(alarmDashboardId.toString());
}
@Test
public void testMobileSettings_tenantLevel() throws Exception {
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
NotificationSettings settings = new NotificationSettings();
settings.setDeliveryMethodsConfigs(Map.of(
NotificationDeliveryMethod.MOBILE_APP, config
));
ResultActions result = doPost("/api/notification/settings", settings)
.andExpect(status().isBadRequest());
assertThat(getErrorMessage(result)).contains("can only be configured by system administrator");
}
private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception { private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception {
SettableFuture<NotificationRequestStats> future = SettableFuture.create(); SettableFuture<NotificationRequestStats> future = SettableFuture.create();
notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, new FutureCallback<>() { notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, new FutureCallback<>() {

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

@ -26,6 +26,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair; import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
@ -93,7 +94,6 @@ 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.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;
@ -140,8 +140,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationRuleProcessor notificationRuleProcessor; private NotificationRuleProcessor notificationRuleProcessor;
@Autowired @Autowired
private DefaultNotifications defaultNotifications;
@Autowired
private DefaultNotificationRulesCache notificationRulesCache; private DefaultNotificationRulesCache notificationRulesCache;
@Autowired @Autowired
private DeviceStateService deviceStateService; private DeviceStateService deviceStateService;

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

1
application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java

@ -37,7 +37,6 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken; import org.thingsboard.server.service.security.model.token.AccessJwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;

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

21
common/actor/src/main/java/org/thingsboard/server/actors/DefaultTbActorSystem.java

@ -156,14 +156,29 @@ public class DefaultTbActorSystem implements TbActorSystem {
@Override @Override
public void broadcastToChildren(TbActorId parent, TbActorMsg msg) { public void broadcastToChildren(TbActorId parent, TbActorMsg msg) {
broadcastToChildren(parent, id -> true, msg); broadcastToChildren(parent, msg, false);
}
@Override
public void broadcastToChildren(TbActorId parent, TbActorMsg msg, boolean highPriority) {
broadcastToChildren(parent, id -> true, msg, highPriority);
} }
@Override @Override
public void broadcastToChildren(TbActorId parent, Predicate<TbActorId> childFilter, TbActorMsg msg) { public void broadcastToChildren(TbActorId parent, Predicate<TbActorId> childFilter, TbActorMsg msg) {
broadcastToChildren(parent, childFilter, msg, false);
}
private void broadcastToChildren(TbActorId parent, Predicate<TbActorId> childFilter, TbActorMsg msg, boolean highPriority) {
Set<TbActorId> children = parentChildMap.get(parent); Set<TbActorId> children = parentChildMap.get(parent);
if (children != null) { if (children != null) {
children.stream().filter(childFilter).forEach(id -> tell(id, msg)); children.stream().filter(childFilter).forEach(id -> {
try {
tell(id, msg, highPriority);
} catch (TbActorNotRegisteredException e) {
log.warn("Actor is missing for {}", id);
}
});
} }
} }
@ -190,6 +205,8 @@ public class DefaultTbActorSystem implements TbActorSystem {
stop(child); stop(child);
} }
} }
parentChildMap.values().forEach(parentChildren -> parentChildren.remove(actorId));
TbActorMailbox mailbox = actors.remove(actorId); TbActorMailbox mailbox = actors.remove(actorId);
if (mailbox != null) { if (mailbox != null) {
mailbox.destroy(null); mailbox.destroy(null);

2
common/actor/src/main/java/org/thingsboard/server/actors/TbActor.java

@ -34,7 +34,7 @@ public interface TbActor {
return InitFailureStrategy.retryWithDelay(5000L * attempt); return InitFailureStrategy.retryWithDelay(5000L * attempt);
} }
default ProcessFailureStrategy onProcessFailure(Throwable t) { default ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) {
if (t instanceof Error) { if (t instanceof Error) {
return ProcessFailureStrategy.stop(); return ProcessFailureStrategy.stop();
} else { } else {

2
common/actor/src/main/java/org/thingsboard/server/actors/TbActorCtx.java

@ -36,6 +36,8 @@ public interface TbActorCtx extends TbActorRef {
void broadcastToChildren(TbActorMsg msg); void broadcastToChildren(TbActorMsg msg);
void broadcastToChildren(TbActorMsg msg, boolean highPriority);
void broadcastToChildrenByType(TbActorMsg msg, EntityType entityType); void broadcastToChildrenByType(TbActorMsg msg, EntityType entityType);
void broadcastToChildren(TbActorMsg msg, Predicate<TbActorId> childFilter); void broadcastToChildren(TbActorMsg msg, Predicate<TbActorId> childFilter);

9
common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java

@ -160,7 +160,7 @@ public final class TbActorMailbox implements TbActorCtx {
destroy(updateException.getCause()); destroy(updateException.getCause());
} catch (Throwable t) { } catch (Throwable t) {
log.debug("[{}] Failed to process message: {}", selfId, msg, t); log.debug("[{}] Failed to process message: {}", selfId, msg, t);
ProcessFailureStrategy strategy = actor.onProcessFailure(t); ProcessFailureStrategy strategy = actor.onProcessFailure(msg, t);
if (strategy.isStop()) { if (strategy.isStop()) {
system.stop(selfId); system.stop(selfId);
} }
@ -190,7 +190,12 @@ public final class TbActorMailbox implements TbActorCtx {
@Override @Override
public void broadcastToChildren(TbActorMsg msg) { public void broadcastToChildren(TbActorMsg msg) {
system.broadcastToChildren(selfId, msg); broadcastToChildren(msg, false);
}
@Override
public void broadcastToChildren(TbActorMsg msg, boolean highPriority) {
system.broadcastToChildren(selfId, msg, highPriority);
} }
@Override @Override

2
common/actor/src/main/java/org/thingsboard/server/actors/TbActorSystem.java

@ -48,6 +48,8 @@ public interface TbActorSystem {
void broadcastToChildren(TbActorId parent, TbActorMsg msg); void broadcastToChildren(TbActorId parent, TbActorMsg msg);
void broadcastToChildren(TbActorId parent, TbActorMsg msg, boolean highPriority);
void broadcastToChildren(TbActorId parent, Predicate<TbActorId> childFilter, TbActorMsg msg); void broadcastToChildren(TbActorId parent, Predicate<TbActorId> childFilter, TbActorMsg msg);
List<TbActorId> filterChildren(TbActorId parent, Predicate<TbActorId> childFilter); List<TbActorId> filterChildren(TbActorId parent, Predicate<TbActorId> childFilter);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java

@ -26,6 +26,8 @@ public interface NotificationSettingsService {
NotificationSettings findNotificationSettings(TenantId tenantId); NotificationSettings findNotificationSettings(TenantId tenantId);
void deleteNotificationSettings(TenantId tenantId);
UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings); UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings);
UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format); UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format);

10
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.user;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List; import java.util.List;
import java.util.Map;
public interface UserService extends EntityDaoService { public interface UserService extends EntityDaoService {
@ -89,4 +91,12 @@ public interface UserService extends EntityDaoService {
void setLastLoginTs(TenantId tenantId, UserId userId); void setLastLoginTs(TenantId tenantId, UserId userId);
void saveMobileSession(TenantId tenantId, UserId userId, String mobileToken, MobileSessionInfo sessionInfo);
Map<String, MobileSessionInfo> findMobileSessions(TenantId tenantId, UserId userId);
MobileSessionInfo findMobileSession(TenantId tenantId, UserId userId, String mobileToken);
void removeMobileSession(TenantId tenantId, String mobileToken);
} }

30
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -19,7 +19,10 @@ import com.google.common.base.Splitter;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import static org.apache.commons.lang3.StringUtils.repeat; import static org.apache.commons.lang3.StringUtils.repeat;
@ -156,6 +159,10 @@ public class StringUtils {
} }
public static boolean equalsAny(String string, String... otherStrings) { public static boolean equalsAny(String string, String... otherStrings) {
return equalsAny(string, Arrays.asList(otherStrings));
}
public static boolean equalsAny(String string, List<String> otherStrings) {
for (String otherString : otherStrings) { for (String otherString : otherStrings) {
if (equals(string, otherString)) { if (equals(string, otherString)) {
return true; return true;
@ -245,4 +252,27 @@ public class StringUtils {
return string.substring(0, maxLength) + truncationMarkerFunc.apply(truncatedSymbols); return string.substring(0, maxLength) + truncationMarkerFunc.apply(truncatedSymbols);
} }
public static List<String> splitByCommaWithoutQuotes(String value) {
List<String> splitValues = List.of(value.trim().split("\\s*,\\s*"));
List<String> result = new ArrayList<>();
char lastWayInputValue = '#';
for (String str : splitValues) {
char startWith = str.charAt(0);
char endWith = str.charAt(str.length() - 1);
// if first value is not quote, so we return values after split
if (startWith != '\'' && startWith != '"') return splitValues;
// if value is not in quote, so we return values after split
if (startWith != endWith) return splitValues;
// if different way values, so don't replace quote and return values after split
if (lastWayInputValue != '#' && startWith != lastWayInputValue) return splitValues;
result.add(str.substring(1, str.length() - 1));
lastWayInputValue = startWith;
}
return result;
}
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.alarm; package org.thingsboard.server.common.data.alarm;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@ -30,6 +31,7 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
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.UserId; import org.thingsboard.server.common.data.id.UserId;
@ -37,6 +39,8 @@ import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID;
/** /**
* Created by ashvayka on 11.05.17. * Created by ashvayka on 11.05.17.
@ -160,4 +164,10 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
} }
} }
@JsonIgnore
public DashboardId getDashboardId() {
return Optional.ofNullable(getDetails()).map(details -> details.get("dashboardId"))
.filter(JsonNode::isTextual).map(id -> new DashboardId(UUID.fromString(id.asText()))).orElse(null);
}
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java

@ -35,6 +35,7 @@ public enum EdgeEventActionType {
RPC_CALL(ActionType.RPC_CALL), RPC_CALL(ActionType.RPC_CALL),
ALARM_ACK(ActionType.ALARM_ACK), ALARM_ACK(ActionType.ALARM_ACK),
ALARM_CLEAR(ActionType.ALARM_CLEAR), ALARM_CLEAR(ActionType.ALARM_CLEAR),
ALARM_DELETE(ActionType.ALARM_DELETE),
ALARM_ASSIGNED(ActionType.ALARM_ASSIGNED), ALARM_ASSIGNED(ActionType.ALARM_ASSIGNED),
ALARM_UNASSIGNED(ActionType.ALARM_UNASSIGNED), ALARM_UNASSIGNED(ActionType.ALARM_UNASSIGNED),
ADDED_COMMENT(ActionType.ADDED_COMMENT), ADDED_COMMENT(ActionType.ADDED_COMMENT),

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

@ -33,6 +33,8 @@ public enum LimitedApi {
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true), CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true), EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false), EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true),
EDGE_UPLINK_MESSAGES_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimitsPerEdge, "Edge uplink messages 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),

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

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

27
common/data/src/main/java/org/thingsboard/server/common/data/mobile/UserMobileInfo.java

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

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java

@ -25,7 +25,8 @@ public enum NotificationDeliveryMethod {
EMAIL("email"), EMAIL("email"),
SMS("SMS"), SMS("SMS"),
SLACK("Slack"), SLACK("Slack"),
MICROSOFT_TEAMS("Microsoft Teams"); MICROSOFT_TEAMS("Microsoft Teams"),
MOBILE_APP("mobile app");
@Getter @Getter
private final String name; private final String name;

10
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java

@ -31,14 +31,20 @@ import java.util.concurrent.atomic.AtomicInteger;
public class NotificationRequestStats { public class NotificationRequestStats {
private final Map<NotificationDeliveryMethod, AtomicInteger> sent; private final Map<NotificationDeliveryMethod, AtomicInteger> sent;
@JsonIgnore
private final AtomicInteger totalSent;
private final Map<NotificationDeliveryMethod, Map<String, String>> errors; private final Map<NotificationDeliveryMethod, Map<String, String>> errors;
@JsonIgnore
private final AtomicInteger totalErrors;
private String error; private String error;
@JsonIgnore @JsonIgnore
private final Map<NotificationDeliveryMethod, Set<Object>> processedRecipients; private final Map<NotificationDeliveryMethod, Set<Object>> processedRecipients;
public NotificationRequestStats() { public NotificationRequestStats() {
this.sent = new ConcurrentHashMap<>(); this.sent = new ConcurrentHashMap<>();
this.totalSent = new AtomicInteger();
this.errors = new ConcurrentHashMap<>(); this.errors = new ConcurrentHashMap<>();
this.totalErrors = new AtomicInteger();
this.processedRecipients = new ConcurrentHashMap<>(); this.processedRecipients = new ConcurrentHashMap<>();
} }
@ -47,13 +53,16 @@ public class NotificationRequestStats {
@JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors, @JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors,
@JsonProperty("error") String error) { @JsonProperty("error") String error) {
this.sent = sent; this.sent = sent;
this.totalSent = null;
this.errors = errors; this.errors = errors;
this.totalErrors = null;
this.error = error; this.error = error;
this.processedRecipients = Collections.emptyMap(); this.processedRecipients = Collections.emptyMap();
} }
public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) {
sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet(); sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet();
totalSent.incrementAndGet();
} }
public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) { public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) {
@ -65,6 +74,7 @@ public class NotificationRequestStats {
errorMessage = error.getClass().getSimpleName(); errorMessage = error.getClass().getSimpleName();
} }
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage); errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage);
totalErrors.incrementAndGet();
} }
public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) { public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) {

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

@ -31,4 +31,5 @@ public enum NotificationType {
RATE_LIMITS, RATE_LIMITS,
EDGE_CONNECTION, EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE EDGE_COMMUNICATION_FAILURE
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
@ -55,6 +56,7 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
private AlarmSeverity alarmSeverity; private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus; private AlarmStatus alarmStatus;
private CustomerId alarmCustomerId; private CustomerId alarmCustomerId;
private DashboardId dashboardId;
@Override @Override
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
@ -94,4 +96,9 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
return alarmOriginator; return alarmOriginator;
} }
@Override
public DashboardId getDashboardId() {
return dashboardId;
}
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map; import java.util.Map;
@ -50,6 +51,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
private AlarmSeverity alarmSeverity; private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus; private AlarmStatus alarmStatus;
private CustomerId alarmCustomerId; private CustomerId alarmCustomerId;
private DashboardId dashboardId;
@Override @Override
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
@ -80,4 +82,9 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
return alarmOriginator; return alarmOriginator;
} }
@Override
public DashboardId getDashboardId() {
return dashboardId;
}
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java

@ -22,6 +22,7 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map; import java.util.Map;
@ -45,6 +46,7 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
private boolean acknowledged; private boolean acknowledged;
private boolean cleared; private boolean cleared;
private CustomerId alarmCustomerId; private CustomerId alarmCustomerId;
private DashboardId dashboardId;
@Override @Override
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
@ -70,4 +72,9 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
return alarmOriginator; return alarmOriginator;
} }
@Override
public DashboardId getDashboardId() {
return dashboardId;
}
} }

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

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification.info;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map; import java.util.Map;
@ -33,4 +34,8 @@ public interface NotificationInfo {
return null; return null;
} }
default DashboardId getDashboardId() {
return null;
}
} }

35
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.settings;
import lombok.Data;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotEmpty;
@Data
public class MobileAppNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig {
private String firebaseServiceAccountCredentialsFileName;
@NotEmpty
private String firebaseServiceAccountCredentials;
@Override
public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.MOBILE_APP;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java

@ -27,7 +27,8 @@ import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method")
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "SLACK", value = SlackNotificationDeliveryMethodConfig.class) @Type(name = "SLACK", value = SlackNotificationDeliveryMethodConfig.class),
@Type(name = "MOBILE_APP", value = MobileAppNotificationDeliveryMethodConfig.class)
}) })
public interface NotificationDeliveryMethodConfig extends Serializable { public interface NotificationDeliveryMethodConfig extends Serializable {

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java

@ -28,7 +28,6 @@ public class NotificationSettings implements Serializable {
@NotNull @NotNull
@Valid @Valid
// location on the screen, shown notifications count, timings of displaying
private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs; private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java

@ -25,7 +25,7 @@ import java.util.Set;
@RequiredArgsConstructor @RequiredArgsConstructor
public enum NotificationTargetType { public enum NotificationTargetType {
PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS)), PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS, NotificationDeliveryMethod.MOBILE_APP)),
SLACK(Set.of(NotificationDeliveryMethod.SLACK)), SLACK(Set.of(NotificationDeliveryMethod.SLACK)),
MICROSOFT_TEAMS(Set.of(NotificationDeliveryMethod.MICROSOFT_TEAMS)); MICROSOFT_TEAMS(Set.of(NotificationDeliveryMethod.MICROSOFT_TEAMS));

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java

@ -34,7 +34,8 @@ import java.util.List;
@Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class), @Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class),
@Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class), @Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class),
@Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class), @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class),
@Type(name = "MICROSOFT_TEAMS", value = MicrosoftTeamsDeliveryMethodNotificationTemplate.class) @Type(name = "MICROSOFT_TEAMS", value = MicrosoftTeamsDeliveryMethodNotificationTemplate.class),
@Type(name = "MOBILE_APP", value = MobileAppDeliveryMethodNotificationTemplate.class)
}) })
@Data @Data
@NoArgsConstructor @NoArgsConstructor

64
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java

@ -0,0 +1,64 @@
/**
* 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.template;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MobileAppDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject {
@NotEmpty
private String subject;
private JsonNode additionalConfig;
private final List<TemplatableValue> templatableValues = List.of(
TemplatableValue.of(this::getBody, this::setBody),
TemplatableValue.of(this::getSubject, this::setSubject)
);
public MobileAppDeliveryMethodNotificationTemplate(MobileAppDeliveryMethodNotificationTemplate other) {
super(other);
this.subject = other.subject;
this.additionalConfig = other.additionalConfig;
}
@Override
public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.MOBILE_APP;
}
@Override
public MobileAppDeliveryMethodNotificationTemplate copy() {
return new MobileAppDeliveryMethodNotificationTemplate(this);
}
@Override
public List<TemplatableValue> getTemplatableValues() {
return templatableValues;
}
}

9
common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java

@ -19,7 +19,14 @@ import lombok.Getter;
public enum UserSettingsType { public enum UserSettingsType {
GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED, NOTIFICATIONS; GENERAL,
VISITED_DASHBOARDS(true),
QUICK_LINKS,
DOC_LINKS,
DASHBOARDS,
GETTING_STARTED,
NOTIFICATIONS,
MOBILE(true);
@Getter @Getter
private final boolean reserved; private final boolean reserved;

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

Loading…
Cancel
Save