diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md
index bffb88ebb7..6d8889f98d 100644
--- a/.github/ISSUE_TEMPLATE/---bug-report.md
+++ b/.github/ISSUE_TEMPLATE/---bug-report.md
@@ -1,9 +1,9 @@
---
name: "\U0001F41E Bug report"
about: Create a report to help us improve
-title: "[Bug] "
-labels: bug
-assignees: ashvayka, vvlladd28
+title: "Your title here"
+labels: ['bug', 'unconfirmed']
+assignees: AndriichnekoDm
---
@@ -12,11 +12,13 @@ A clear and concise description of what the bug is.
**Your Server Environment**
-* demo.thingsboard.io
-* cloud.thingsboard.io
+* [https://demo.thingsboard.io](demo.thingsboard.io)
+* [https://thingsboard.cloud](thingsboard.cloud)
* own setup
- * cloud or local infrastructure or docker deployment
+ * Deployment: monolith or microservices
+ * Deployment type: deb, rpm, exe, docker-compose, k8s, ami
* ThingsBoard Version
+ * Community or Professional Edition
* OS Name and Version
**Your Client Environment**
@@ -54,7 +56,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen.
**Screenshots**
-If applicable, add screenshots to help explain your problem.
+If applicable, please add screenshots to help explain your problem.
**Additional context**
-Add any other context about the problem here.
+Please feel free to add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 8730336bba..c73f810cd8 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,9 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
-title: "[Feature Request]"
-labels: feature
-assignees: ikulikov
+title: "Your title here"
+labels: ['feature']
+assignees: 'AndriichnekoDm'
---
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index 99d82ca698..2f15690f68 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -1,9 +1,9 @@
---
name: Question
-about: Describe your questions in details
-title: "[Question] your title here"
-labels: question
-assignees: ashvayka
+about: Describe your questions in detail
+title: "Your title here"
+labels: ['question']
+assignees: 'AndriichnekoDm'
---
@@ -16,7 +16,7 @@ assignees: ashvayka
* Generic
**Description**
-A clear and concise details.
+Clear and concise details.
**Environment**
diff --git a/.github/workflows/license-header-format.yml b/.github/workflows/license-header-format.yml
new file mode 100644
index 0000000000..20b5e7f063
--- /dev/null
+++ b/.github/workflows/license-header-format.yml
@@ -0,0 +1,54 @@
+#
+# 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.
+#
+
+name: License header format
+
+on:
+ push:
+ branches:
+ - 'master'
+ - 'develop/3*'
+ - 'hotfix/3*'
+
+jobs:
+ license-format:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'corretto' # https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions
+ java-version: '21'
+ cache: 'maven' # https://github.com/actions/setup-java?tab=readme-ov-file#caching-sbt-dependencies
+
+ - name: License header format
+ run: mvn -T 1C license:format
+
+ - name: License header format (msa/black-box-tests/)
+ run: mvn -T 1C license:format -f msa/black-box-tests/
+
+ - name: Set Git user information
+ run: |
+ git config user.name "ThingsBoard Bot"
+ git config user.email "noreply@thingsboard.io"
+
+ - name: Check and push changes
+ run: |
+ git diff --exit-code || git commit -am "License header format" && git push
diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json
index fb7c6e1ee2..5dce630e64 100644
--- a/application/src/main/data/json/system/widget_bundles/control_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json
@@ -2,13 +2,13 @@
"widgetsBundle": {
"alias": "control_widgets",
"title": "Control widgets",
- "image": "data:image/png;base64,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",
+ "image": "tb-image:Y29udHJvbF93aWRnZXRzX3N5c3RlbV9idW5kbGVfaW1hZ2UucG5n:IkNvbnRyb2wgd2lkZ2V0cyIgc3lzdGVtIGJ1bmRsZSBpbWFnZQ==;data:image/png;base64,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",
"description": "Various interactive widgets to control the behavior and state of the IoT devices.",
"order": 8000,
- "externalId": null,
"name": "Control widgets"
},
"widgetTypeFqns": [
+ "single_switch",
"control_widgets.switch_control",
"control_widgets.slide_toggle_control",
"control_widgets.round_switch",
diff --git a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
index 06ee881c0a..75814a547a 100644
--- a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
+++ b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
@@ -20,7 +20,7 @@
"latestDataKeySettingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-bar-chart-with-labels-basic-config",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":2592000000,\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"MMMM y\",\"lastUpdateAgo\":false,\"custom\":true},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":\"MONTH\",\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"MMMM y\",\"lastUpdateAgo\":false,\"custom\":true},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
},
"tags": [
"bar chart",
diff --git a/application/src/main/data/json/system/widget_types/single_switch.json b/application/src/main/data/json/system/widget_types/single_switch.json
new file mode 100644
index 0000000000..4a62cc0598
--- /dev/null
+++ b/application/src/main/data/json/system/widget_types/single_switch.json
@@ -0,0 +1,35 @@
+{
+ "fqn": "single_switch",
+ "name": "Single Switch",
+ "deprecated": false,
+ "image": "tb-image:c2luZ2xlLXN3aXRjaC5zdmc=:IlNpbmdsZSBTd2l0Y2giIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAxIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8zNjM5XzE1NTY3NCkiPgo8cmVjdCB4PSIwLjUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iMSIgeT0iMC41IiB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgcng9IjMuNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHJlY3QgeD0iNTUiIHk9IjcwIiB3aWR0aD0iMzguMDk1MiIgaGVpZ2h0PSIxOS45NTQ3IiByeD0iOS45NzczMyIgZmlsbD0iIzU0NjlGRiIvPgo8Y2lyY2xlIGN4PSI4My4xMTY0IiBjeT0iNzkuOTc3MiIgcj0iOC4xNjMyNyIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEwNi42OTQgODEuNzUzNEMxMDYuNjk0IDgxLjUzMzcgMTA2LjY2IDgxLjMzODQgMTA2LjU5MiA4MS4xNjc1QzEwNi41MjggODAuOTk2NiAxMDYuNDE0IDgwLjg0MDMgMTA2LjI0OCA4MC42OTg3QzEwNi4wODIgODAuNTU3MSAxMDUuODQ3IDgwLjQyMDQgMTA1LjU0NCA4MC4yODg2QzEwNS4yNDcgODAuMTUxOSAxMDQuODY2IDgwLjAxMjcgMTA0LjQwMiA3OS44NzExQzEwMy44OTQgNzkuNzE0OCAxMDMuNDI1IDc5LjU0MTUgMTAyLjk5NiA3OS4zNTExQzEwMi41NzEgNzkuMTU1OCAxMDIuMiA3OC45MzEyIDEwMS44ODIgNzguNjc3MkMxMDEuNTY1IDc4LjQxODUgMTAxLjMxOCA3OC4xMjMgMTAxLjE0MyA3Ny43OTFDMTAwLjk2NyA3Ny40NTQxIDEwMC44NzkgNzcuMDY1OSAxMDAuODc5IDc2LjYyNjVDMTAwLjg3OSA3Ni4xOTE5IDEwMC45NjkgNzUuNzk2NCAxMDEuMTUgNzUuNDM5OUMxMDEuMzM1IDc1LjA4MzUgMTAxLjU5NyA3NC43NzU5IDEwMS45MzQgNzQuNTE3MUMxMDIuMjc1IDc0LjI1MzQgMTAyLjY3OCA3NC4wNTA4IDEwMy4xNDIgNzMuOTA5MkMxMDMuNjA2IDczLjc2MjcgMTA0LjExOSA3My42ODk1IDEwNC42OCA3My42ODk1QzEwNS40NzEgNzMuNjg5NSAxMDYuMTUyIDczLjgzNTkgMTA2LjcyNCA3NC4xMjg5QzEwNy4zIDc0LjQyMTkgMTA3Ljc0MiA3NC44MTQ5IDEwOC4wNDkgNzUuMzA4MUMxMDguMzYyIDc1LjgwMTMgMTA4LjUxOCA3Ni4zNDU3IDEwOC41MTggNzYuOTQxNEgxMDYuNjk0QzEwNi42OTQgNzYuNTg5OCAxMDYuNjE5IDc2LjI3OTggMTA2LjQ2NyA3Ni4wMTEyQzEwNi4zMjEgNzUuNzM3OCAxMDYuMDk2IDc1LjUyMjkgMTA1Ljc5MyA3NS4zNjY3QzEwNS40OTYgNzUuMjEwNCAxMDUuMTE3IDc1LjEzMjMgMTA0LjY1OCA3NS4xMzIzQzEwNC4yMjQgNzUuMTMyMyAxMDMuODYyIDc1LjE5ODIgMTAzLjU3NCA3NS4zMzAxQzEwMy4yODYgNzUuNDYxOSAxMDMuMDcxIDc1LjY0MDEgMTAyLjkzIDc1Ljg2NDdDMTAyLjc4OCA3Ni4wODk0IDEwMi43MTcgNzYuMzQzMyAxMDIuNzE3IDc2LjYyNjVDMTAyLjcxNyA3Ni44MjY3IDEwMi43NjQgNzcuMDA5OCAxMDIuODU2IDc3LjE3NThDMTAyLjk0OSA3Ny4zMzY5IDEwMy4wOTEgNzcuNDg4MyAxMDMuMjgxIDc3LjYyOTlDMTAzLjQ3MiA3Ny43NjY2IDEwMy43MTEgNzcuODk2IDEwMy45OTkgNzguMDE4MUMxMDQuMjg3IDc4LjE0MDEgMTA0LjYyNiA3OC4yNTczIDEwNS4wMTcgNzguMzY5NkMxMDUuNjA4IDc4LjU0NTQgMTA2LjEyMyA3OC43NDA3IDEwNi41NjIgNzguOTU1NkMxMDcuMDAyIDc5LjE2NTUgMTA3LjM2OCA3OS40MDQ4IDEwNy42NjEgNzkuNjczM0MxMDcuOTU0IDc5Ljk0MTkgMTA4LjE3NCA4MC4yNDcxIDEwOC4zMiA4MC41ODg5QzEwOC40NjcgODAuOTI1OCAxMDguNTQgODEuMzA5MSAxMDguNTQgODEuNzM4OEMxMDguNTQgODIuMTg4IDEwOC40NSA4Mi41OTMzIDEwOC4yNjkgODIuOTU0NkMxMDguMDg4IDgzLjMxMSAxMDcuODMgODMuNjE2MiAxMDcuNDkzIDgzLjg3MDFDMTA3LjE2MSA4NC4xMTkxIDEwNi43NiA4NC4zMTIgMTA2LjI5MiA4NC40NDg3QzEwNS44MjggODQuNTgwNiAxMDUuMzEgODQuNjQ2NSAxMDQuNzM5IDg0LjY0NjVDMTA0LjIyNiA4NC42NDY1IDEwMy43MjEgODQuNTc4MSAxMDMuMjIzIDg0LjQ0MTRDMTAyLjcyOSA4NC4zMDQ3IDEwMi4yOCA4NC4wOTcyIDEwMS44NzUgODMuODE4OEMxMDEuNDcgODMuNTM1NiAxMDEuMTQ3IDgzLjE4NDEgMTAwLjkwOCA4Mi43NjQyQzEwMC42NjkgODIuMzM5NCAxMDAuNTQ5IDgxLjg0MzggMTAwLjU0OSA4MS4yNzczSDEwMi4zODhDMTAyLjM4OCA4MS42MjQgMTAyLjQ0NiA4MS45MTk0IDEwMi41NjMgODIuMTYzNkMxMDIuNjg2IDgyLjQwNzcgMTAyLjg1NCA4Mi42MDc5IDEwMy4wNjkgODIuNzY0MkMxMDMuMjg0IDgyLjkxNTUgMTAzLjUzMyA4My4wMjc4IDEwMy44MTYgODMuMTAxMUMxMDQuMTA0IDgzLjE3NDMgMTA0LjQxMiA4My4yMTA5IDEwNC43MzkgODMuMjEwOUMxMDUuMTY4IDgzLjIxMDkgMTA1LjUyNyA4My4xNDk5IDEwNS44MTUgODMuMDI3OEMxMDYuMTA4IDgyLjkwNTggMTA2LjMyOCA4Mi43MzQ5IDEwNi40NzUgODIuNTE1MUMxMDYuNjIxIDgyLjI5NTQgMTA2LjY5NCA4Mi4wNDE1IDEwNi42OTQgODEuNzUzNFpNMTEyLjE0NCA4Mi43NDIyTDExMy45NzUgNzYuNTc1MkgxMTUuMTAzTDExNC43OTUgNzguNDIwOUwxMTIuOTQ5IDg0LjVIMTExLjkzOEwxMTIuMTQ0IDgyLjc0MjJaTTExMS4wNjcgNzYuNTc1MkwxMTIuNDk1IDgyLjc3MTVMMTEyLjYxMiA4NC41SDExMS40ODRMMTA5LjMzOCA3Ni41NzUySDExMS4wNjdaTTExNi44MTYgODIuNjk4MkwxMTguMjAxIDc2LjU3NTJIMTE5LjkyMkwxMTcuNzgzIDg0LjVIMTE2LjY1NUwxMTYuODE2IDgyLjY5ODJaTTExNS4yOTMgNzYuNTc1MkwxMTcuMTAyIDgyLjY2ODlMMTE3LjMyOSA4NC41SDExNi4zMThMMTE0LjQ1MSA3OC40MTM2TDExNC4xNDMgNzYuNTc1MkgxMTUuMjkzWk0xMjMuMDEzIDc2LjU3NTJWODQuNUgxMjEuMjRWNzYuNTc1MkgxMjMuMDEzWk0xMjEuMTIzIDc0LjQ5NTFDMTIxLjEyMyA3NC4yMjY2IDEyMS4yMTEgNzQuMDA0NCAxMjEuMzg3IDczLjgyODZDMTIxLjU2NyA3My42NDc5IDEyMS44MTYgNzMuNTU3NiAxMjIuMTM0IDczLjU1NzZDMTIyLjQ0NiA3My41NTc2IDEyMi42OTMgNzMuNjQ3OSAxMjIuODc0IDczLjgyODZDMTIzLjA1NCA3NC4wMDQ0IDEyMy4xNDUgNzQuMjI2NiAxMjMuMTQ1IDc0LjQ5NTFDMTIzLjE0NSA3NC43NTg4IDEyMy4wNTQgNzQuOTc4NSAxMjIuODc0IDc1LjE1NDNDMTIyLjY5MyA3NS4zMzAxIDEyMi40NDYgNzUuNDE4IDEyMi4xMzQgNzUuNDE4QzEyMS44MTYgNzUuNDE4IDEyMS41NjcgNzUuMzMwMSAxMjEuMzg3IDc1LjE1NDNDMTIxLjIxMSA3NC45Nzg1IDEyMS4xMjMgNzQuNzU4OCAxMjEuMTIzIDc0LjQ5NTFaTTEyOC41NzkgNzYuNTc1MlY3Ny44NjQzSDEyNC4xMTFWNzYuNTc1MkgxMjguNTc5Wk0xMjUuNCA3NC42MzQzSDEyNy4xNjZWODIuMzEwMUMxMjcuMTY2IDgyLjU1NDIgMTI3LjIgODIuNzQyMiAxMjcuMjY4IDgyLjg3NEMxMjcuMzQxIDgzLjAwMSAxMjcuNDQxIDgzLjA4NjQgMTI3LjU2OCA4My4xMzA0QzEyNy42OTUgODMuMTc0MyAxMjcuODQ0IDgzLjE5NjMgMTI4LjAxNSA4My4xOTYzQzEyOC4xMzcgODMuMTk2MyAxMjguMjU0IDgzLjE4OSAxMjguMzY3IDgzLjE3NDNDMTI4LjQ3OSA4My4xNTk3IDEyOC41NjkgODMuMTQ1IDEyOC42MzggODMuMTMwNEwxMjguNjQ1IDg0LjQ3OEMxMjguNDk5IDg0LjUyMiAxMjguMzI4IDg0LjU2MSAxMjguMTMyIDg0LjU5NTJDMTI3Ljk0MiA4NC42Mjk0IDEyNy43MjIgODQuNjQ2NSAxMjcuNDczIDg0LjY0NjVDMTI3LjA2OCA4NC42NDY1IDEyNi43MDkgODQuNTc1NyAxMjYuMzk2IDg0LjQzNDFDMTI2LjA4NCA4NC4yODc2IDEyNS44NCA4NC4wNTA4IDEyNS42NjQgODMuNzIzNkMxMjUuNDg4IDgzLjM5NjUgMTI1LjQgODIuOTYxOSAxMjUuNCA4Mi40MTk5Vjc0LjYzNDNaTTEzMy4xNzkgODMuMjQwMkMxMzMuNDY3IDgzLjI0MDIgMTMzLjcyNiA4My4xODQxIDEzMy45NTUgODMuMDcxOEMxMzQuMTg5IDgyLjk1NDYgMTM0LjM3NyA4Mi43OTM1IDEzNC41MTkgODIuNTg4NEMxMzQuNjY2IDgyLjM4MzMgMTM0Ljc0NiA4Mi4xNDY1IDEzNC43NjEgODEuODc3OUgxMzYuNDIzQzEzNi40MTQgODIuMzkwNiAxMzYuMjYyIDgyLjg1NjkgMTM1Ljk2OSA4My4yNzY5QzEzNS42NzYgODMuNjk2OCAxMzUuMjg4IDg0LjAzMTIgMTM0LjgwNSA4NC4yODAzQzEzNC4zMjEgODQuNTI0NCAxMzMuNzg3IDg0LjY0NjUgMTMzLjIwMSA4NC42NDY1QzEzMi41OTUgODQuNjQ2NSAxMzIuMDY4IDg0LjU0MzkgMTMxLjYxOSA4NC4zMzg5QzEzMS4xNjkgODQuMTI4OSAxMzAuNzk2IDgzLjg0MDggMTMwLjQ5OCA4My40NzQ2QzEzMC4yIDgzLjEwODQgMTI5Ljk3NiA4Mi42ODYgMTI5LjgyNCA4Mi4yMDc1QzEyOS42NzggODEuNzI5IDEyOS42MDQgODEuMjE2MyAxMjkuNjA0IDgwLjY2OTRWODAuNDEzMUMxMjkuNjA0IDc5Ljg2NjIgMTI5LjY3OCA3OS4zNTM1IDEyOS44MjQgNzguODc1QzEyOS45NzYgNzguMzkxNiAxMzAuMiA3Ny45NjY4IDEzMC40OTggNzcuNjAwNkMxMzAuNzk2IDc3LjIzNDQgMTMxLjE2OSA3Ni45NDg3IDEzMS42MTkgNzYuNzQzN0MxMzIuMDY4IDc2LjUzMzcgMTMyLjU5MyA3Ni40Mjg3IDEzMy4xOTMgNzYuNDI4N0MxMzMuODI4IDc2LjQyODcgMTM0LjM4NSA3Ni41NTU3IDEzNC44NjMgNzYuODA5NkMxMzUuMzQyIDc3LjA1ODYgMTM1LjcxOCA3Ny40MDc3IDEzNS45OTEgNzcuODU2OUMxMzYuMjcgNzguMzAxMyAxMzYuNDE0IDc4LjgxODggMTM2LjQyMyA3OS40MDk3SDEzNC43NjFDMTM0Ljc0NiA3OS4xMTY3IDEzNC42NzMgNzguODUzIDEzNC41NDEgNzguNjE4N0MxMzQuNDE0IDc4LjM3OTQgMTM0LjIzMyA3OC4xODkgMTMzLjk5OSA3OC4wNDc0QzEzMy43NyA3Ny45MDU4IDEzMy40OTQgNzcuODM1IDEzMy4xNzEgNzcuODM1QzEzMi44MTUgNzcuODM1IDEzMi41MiA3Ny45MDgyIDEzMi4yODUgNzguMDU0N0MxMzIuMDUxIDc4LjE5NjMgMTMxLjg2OCA3OC4zOTE2IDEzMS43MzYgNzguNjQwNkMxMzEuNjA0IDc4Ljg4NDggMTMxLjUwOSA3OS4xNjA2IDEzMS40NSA3OS40NjgzQzEzMS4zOTYgNzkuNzcxIDEzMS4zNyA4MC4wODU5IDEzMS4zNyA4MC40MTMxVjgwLjY2OTRDMTMxLjM3IDgwLjk5NjYgMTMxLjM5NiA4MS4zMTQgMTMxLjQ1IDgxLjYyMTZDMTMxLjUwNCA4MS45MjkyIDEzMS41OTcgODIuMjA1MSAxMzEuNzI5IDgyLjQ0OTJDMTMxLjg2NSA4Mi42ODg1IDEzMi4wNTEgODIuODgxMyAxMzIuMjg1IDgzLjAyNzhDMTMyLjUyIDgzLjE2OTQgMTMyLjgxNyA4My4yNDAyIDEzMy4xNzkgODMuMjQwMlpNMTM5LjUyMSA3My4yNVY4NC41SDEzNy43NjRWNzMuMjVIMTM5LjUyMVpNMTM5LjIxNCA4MC4yNDQ2TDEzOC42NDMgODAuMjM3M0MxMzguNjQ3IDc5LjY5MDQgMTM4LjcyMyA3OS4xODUxIDEzOC44NyA3OC43MjEyQzEzOS4wMjEgNzguMjU3MyAxMzkuMjMxIDc3Ljg1NDUgMTM5LjUgNzcuNTEyN0MxMzkuNzczIDc3LjE2NiAxNDAuMSA3Ni44OTk5IDE0MC40ODEgNzYuNzE0NEMxNDAuODYyIDc2LjUyMzkgMTQxLjI4NCA3Ni40Mjg3IDE0MS43NDggNzYuNDI4N0MxNDIuMTM5IDc2LjQyODcgMTQyLjQ5IDc2LjQ4MjQgMTQyLjgwMyA3Ni41ODk4QzE0My4xMiA3Ni42OTczIDE0My4zOTQgNzYuODcwNiAxNDMuNjIzIDc3LjEwOTlDMTQzLjg1MyA3Ny4zNDQyIDE0NC4wMjYgNzcuNjUxOSAxNDQuMTQzIDc4LjAzMjdDMTQ0LjI2NSA3OC40MDg3IDE0NC4zMjYgNzguODY3NyAxNDQuMzI2IDc5LjQwOTdWODQuNUgxNDIuNTU0Vjc5LjM5NUMxNDIuNTU0IDc5LjAxNDIgMTQyLjQ5OCA3OC43MTE0IDE0Mi4zODUgNzguNDg2OEMxNDIuMjc4IDc4LjI2MjIgMTQyLjExOSA3OC4xMDExIDE0MS45MDkgNzguMDAzNEMxNDEuNjk5IDc3LjkwMDkgMTQxLjQ0MyA3Ny44NDk2IDE0MS4xNCA3Ny44NDk2QzE0MC44MjMgNzcuODQ5NiAxNDAuNTQyIDc3LjkxMzEgMTQwLjI5OCA3OC4wNEMxNDAuMDU5IDc4LjE2NyAxMzkuODU4IDc4LjM0MDMgMTM5LjY5NyA3OC41NjAxQzEzOS41MzYgNzguNzc5OCAxMzkuNDE0IDc5LjAzMzcgMTM5LjMzMSA3OS4zMjE4QzEzOS4yNTMgNzkuNjA5OSAxMzkuMjE0IDc5LjkxNzUgMTM5LjIxNCA4MC4yNDQ2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzM2MzlfMTU1Njc0Ij4KPHJlY3QgeD0iMC41IiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjQiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==",
+ "description": "Sends the command to the device or updates attribute/time-series when the user toggles the slider. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when turn on/off states.",
+ "descriptor": {
+ "type": "rpc",
+ "sizeX": 3.5,
+ "sizeY": 1,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '80px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "",
+ "dataKeySettingsSchema": "{}\n",
+ "settingsDirective": "tb-single-switch-widget-settings",
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-single-switch-basic-config",
+ "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Single Switch\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"configMode\":\"basic\"}"
+ },
+ "tags": [
+ "command",
+ "downlink",
+ "device configuration",
+ "device control",
+ "invocation",
+ "remote method",
+ "remote function",
+ "interface",
+ "subroutine call",
+ "inter-process communication",
+ "server request"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/slide_toggle_control.json b/application/src/main/data/json/system/widget_types/slide_toggle_control.json
index 208c5536c4..5909ba7a9f 100644
--- a/application/src/main/data/json/system/widget_types/slide_toggle_control.json
+++ b/application/src/main/data/json/system/widget_types/slide_toggle_control.json
@@ -1,8 +1,8 @@
{
"fqn": "control_widgets.slide_toggle_control",
"name": "Slide Toggle Control",
- "deprecated": false,
- "image": "data:image/png;base64,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",
+ "deprecated": true,
+ "image": "tb-image:c2xpZGVfdG9nZ2xlX2NvbnRyb2xfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlNsaWRlIFRvZ2dsZSBDb250cm9sIiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,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",
"description": "Sends the RPC call to the device when the user toggles the slider. Appearance widget settings will enable you to configure how to fetch the initial value of the slider.",
"descriptor": {
"type": "rpc",
@@ -17,7 +17,6 @@
"settingsDirective": "tb-slide-toggle-widget-settings",
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":false,\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\",\"title\":\"Slide toggle control\",\"retrieveValueMethod\":\"rpc\",\"valueKey\":\"value\",\"parseValueFunction\":\"return data ? true : false;\",\"convertValueFunction\":\"return value;\",\"requestPersistent\":false,\"labelPosition\":\"after\",\"sliderColor\":\"accent\"},\"title\":\"Slide Toggle Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2,\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\"}"
},
- "externalId": null,
"tags": [
"command",
"downlink",
diff --git a/application/src/main/data/upgrade/3.6.2/schema_update.sql b/application/src/main/data/upgrade/3.6.2/schema_update.sql
index a18b5e1069..6ae5e45134 100644
--- a/application/src/main/data/upgrade/3.6.2/schema_update.sql
+++ b/application/src/main/data/upgrade/3.6.2/schema_update.sql
@@ -21,3 +21,10 @@ DROP INDEX IF EXISTS idx_rule_node_type_configuration_version;
CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version);
-- RULE NODE INDEXES UPDATE END
+
+-- RULE NODE QUEUE UPDATE START
+
+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;
+
+-- RULE NODE QUEUE UPDATE END
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 709f4be250..31d9e477ac 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -29,7 +29,6 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
-import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.SmsService;
@@ -101,6 +100,7 @@ import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
+import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 3a0ba8408a..58bd82abb3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -25,10 +25,6 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.LinkedHashMapRemoveEldest;
-import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
-import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
-import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg;
-import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
@@ -61,7 +57,14 @@ import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
+import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
+import org.thingsboard.server.common.msg.rpc.RemoveRpcActorMsg;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
+import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
@@ -87,10 +90,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
-import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
-import org.thingsboard.server.common.msg.rpc.RemoveRpcActorMsg;
import org.thingsboard.server.service.rpc.RpcSubmitStrategy;
-import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import javax.annotation.Nullable;
@@ -173,7 +173,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private EdgeId findRelatedEdgeId() {
List result =
- systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.EDGE_TYPE, RelationTypeGroup.COMMON);
+ systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON);
if (result != null && result.size() > 0) {
EntityRelation relationToEdge = result.get(0);
if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index badac3509e..cc6653f4c5 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -76,6 +76,7 @@ import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -86,6 +87,7 @@ import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
+import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
import org.thingsboard.server.dao.notification.NotificationRequestService;
@@ -186,7 +188,7 @@ class DefaultTbContext implements TbContext {
@Override
public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator());
enqueue(tpi, tbMsg, onFailure, onSuccess);
}
@@ -311,7 +313,7 @@ class DefaultTbContext implements TbContext {
@Override
public boolean isLocalEntity(EntityId entityId) {
- return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), entityId).isMyPartition();
+ return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), entityId).isMyPartition();
}
private void scheduleMsgWithDelay(TbActorMsg msg, long delayInMs, TbActorRef target) {
@@ -510,6 +512,11 @@ class DefaultTbContext implements TbContext {
return ruleChainName;
}
+ @Override
+ public String getQueueName() {
+ return getSelf().getQueueName();
+ }
+
@Override
public TenantId getTenantId() {
return nodeCtx.getTenantId();
@@ -902,6 +909,16 @@ class DefaultTbContext implements TbContext {
return mainCtx.getEntityService();
}
+ @Override
+ public EventService getEventService() {
+ return mainCtx.getEventService();
+ }
+
+ @Override
+ public AuditLogService getAuditLogService() {
+ return mainCtx.getAuditLogService();
+ }
+
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index 28e6ad190c..e1ccc7dbf5 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -261,7 +261,7 @@ public class TenantActor extends RuleChainManagerActor {
edgeRpcService.updateEdge(tenantId, edge);
}
}
- if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent()) {
+ if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent() && isMyPartition(msg.getEntityId())) {
DeviceId deviceId = (DeviceId) msg.getEntityId();
onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true);
deletedDevices.add(deviceId);
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index f36f63f069..c40760dda3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -16,14 +16,13 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
+import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DataAccessException;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -376,9 +375,14 @@ public abstract class BaseController {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
- } else {
- return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
+ } else if (exception instanceof DataAccessException) {
+ String errorType = exception.getClass().getSimpleName();
+ if (!logControllerErrorStackTrace) { // not to log the error twice
+ log.warn("Database error: {} - {}", errorType, ExceptionUtils.getRootCauseMessage(exception));
+ }
+ return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
+ return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
}
/**
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
index 9abeb57e71..4d11a388ff 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
@@ -20,6 +20,7 @@ import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -43,6 +44,8 @@ import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_ALLOWED_VALUES;
+import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
@@ -119,14 +122,16 @@ public class EntityQueryController extends BaseController {
@ApiParam(value = "Include all unique time-series keys to the result.")
@RequestParam("timeseries") boolean isTimeseries,
@ApiParam(value = "Include all unique attribute keys to the result.")
- @RequestParam("attributes") boolean isAttributes) throws ThingsboardException {
+ @RequestParam("attributes") boolean isAttributes,
+ @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES)
+ @RequestParam(value = "scope", required = false) String scope) throws ThingsboardException {
TenantId tenantId = getTenantId();
checkNotNull(query);
EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE);
}
- return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes);
+ return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
index 295b8cad99..288828f046 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -45,6 +45,8 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.common.data.kv.AggregationParams;
+import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
@@ -310,8 +312,12 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "startTs") Long startTs,
@ApiParam(value = "A long value representing the end timestamp of the time range in milliseconds, UTC.")
@RequestParam(name = "endTs") Long endTs,
+ @ApiParam(value = "A string value representing the type fo the interval.", allowableValues = "MILLISECONDS, WEEK, WEEK_ISO, MONTH, QUARTER")
+ @RequestParam(name = "intervalType", required = false) IntervalType intervalType,
@ApiParam(value = "A long value representing the aggregation interval range in milliseconds.")
@RequestParam(name = "interval", defaultValue = "0") Long interval,
+ @ApiParam(value = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.")
+ @RequestParam(name = "timeZone", required = false) String timeZone,
@ApiParam(value = "An integer value that represents a max number of timeseries data points to fetch." +
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", defaultValue = "100")
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@@ -325,11 +331,16 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> {
- // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
- Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
- List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy))
- .collect(Collectors.toList());
-
+ AggregationParams params;
+ Aggregation agg = Aggregation.valueOf(aggStr);
+ if (Aggregation.NONE.equals(agg)) {
+ params = AggregationParams.none();
+ } else if (intervalType == null || IntervalType.MILLISECONDS.equals(intervalType)) {
+ params = interval == 0L ? AggregationParams.none() : AggregationParams.milliseconds(agg, interval);
+ } else {
+ params = AggregationParams.calendar(agg, intervalType, timeZone);
+ }
+ List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, params, limit, orderBy)).collect(Collectors.toList());
Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());
});
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 448e3e3fa3..915c697e59 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -209,6 +209,7 @@ public class WidgetTypeController extends AutoCommitController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
+ @Deprecated
public List getBundleWidgetTypesByBundleAlias(
@ApiParam(value = "System or Tenant", required = true)
@RequestParam boolean isSystem,
@@ -241,6 +242,7 @@ public class WidgetTypeController extends AutoCommitController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
+ @Deprecated
public List getBundleWidgetTypesDetailsByBundleAlias(
@ApiParam(value = "System or Tenant", required = true)
@RequestParam boolean isSystem,
@@ -256,7 +258,7 @@ public class WidgetTypeController extends AutoCommitController {
return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundle.getId()));
}
- @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypes)",
+ @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetails)",
notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@@ -292,6 +294,7 @@ public class WidgetTypeController extends AutoCommitController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
+ @Deprecated
public List getBundleWidgetTypesInfosByBundleAlias(
@ApiParam(value = "System or Tenant", required = true)
@RequestParam boolean isSystem,
@@ -345,6 +348,7 @@ public class WidgetTypeController extends AutoCommitController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET)
@ResponseBody
+ @Deprecated
public WidgetType getWidgetTypeByBundleAliasAndTypeAlias(
@ApiParam(value = "System or Tenant", required = true)
@RequestParam boolean isSystem,
@@ -365,7 +369,7 @@ public class WidgetTypeController extends AutoCommitController {
}
@ApiOperation(value = "Get Widget Type (getWidgetType)",
- notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
+ notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER, hidden = true)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetType", params = {"fqn"}, method = RequestMethod.GET)
@ResponseBody
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index a598a3b981..84da68385e 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -189,6 +189,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
+ scannedComponent.setHasQueueName(ruleNodeAnnotation.hasQueueName());
NodeDefinition nodeDefinition = prepareNodeDefinition(clazz, ruleNodeAnnotation);
ObjectNode configurationDescriptor = JacksonUtil.newObjectNode();
JsonNode node = JacksonUtil.valueToTree(nodeDefinition);
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
index b3aba37632..03fb982d6d 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
@@ -216,6 +216,9 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService {
case ALARM:
alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg);
break;
+ case ALARM_COMMENT:
+ alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg);
+ break;
case RELATION:
relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg);
break;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
index 351f42d1fc..ca7048c7e9 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
@@ -23,10 +23,12 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.relation.EntityRelation;
@@ -43,8 +45,6 @@ import org.thingsboard.server.dao.user.UserServiceImpl;
import javax.annotation.PostConstruct;
-import static org.thingsboard.server.service.entitiy.DefaultTbNotificationEntityService.edgeTypeByActionType;
-
/**
* This event listener does not support async event processing because relay on ThreadLocal
* Another possible approach is to implement a special annotation and a bunch of classes similar to TransactionalApplicationListener
@@ -79,9 +79,12 @@ public class EdgeEventSourcingListener {
return;
}
log.trace("[{}] SaveEntityEvent called: {}", event.getTenantId(), event);
- EdgeEventActionType action = Boolean.TRUE.equals(event.getAdded()) ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED;
+ boolean isCreated = Boolean.TRUE.equals(event.getCreated());
+ String body = getBodyMsgForEntityEvent(event.getEntity());
+ EdgeEventType type = getEdgeEventTypeForEntityEvent(event.getEntity());
+ EdgeEventActionType action = getActionForEntityEvent(event.getEntity(), isCreated);
tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), null, event.getEntityId(),
- null, null, action, edgeSynchronizationManager.getEdgeId().get());
+ body, type, action, edgeSynchronizationManager.getEdgeId().get());
} catch (Exception e) {
log.error("[{}] failed to process SaveEntityEvent: {}", event.getTenantId(), event, e);
}
@@ -91,20 +94,29 @@ public class EdgeEventSourcingListener {
public void handleEvent(DeleteEntityEvent> event) {
try {
log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event);
+ EdgeEventType type = getEdgeEventTypeForEntityEvent(event.getEntity());
+ EdgeEventActionType actionType = getEdgeEventActionTypeForEntityEvent(event.getEntity());
tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), null, event.getEntityId(),
- JacksonUtil.toString(event.getEntity()), null, EdgeEventActionType.DELETED,
+ JacksonUtil.toString(event.getEntity()), type, actionType,
edgeSynchronizationManager.getEdgeId().get());
} catch (Exception e) {
log.error("[{}] failed to process DeleteEntityEvent: {}", event.getTenantId(), event, e);
}
}
+ private EdgeEventActionType getEdgeEventActionTypeForEntityEvent(Object entity) {
+ if (entity instanceof AlarmComment) {
+ return EdgeEventActionType.DELETED_COMMENT;
+ }
+ return EdgeEventActionType.DELETED;
+ }
+
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(ActionEntityEvent event) {
try {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), event.getEdgeId(), event.getEntityId(),
- event.getBody(), null, edgeTypeByActionType(event.getActionType()),
+ event.getBody(), null, EdgeUtils.getEdgeEventActionTypeByActionType(event.getActionType()),
edgeSynchronizationManager.getEdgeId().get());
} catch (Exception e) {
log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e);
@@ -125,7 +137,7 @@ public class EdgeEventSourcingListener {
}
log.trace("[{}] RelationActionEvent called: {}", event.getTenantId(), event);
tbClusterService.sendNotificationMsgToEdge(event.getTenantId(), null, null,
- JacksonUtil.toString(relation), EdgeEventType.RELATION, edgeTypeByActionType(event.getActionType()),
+ JacksonUtil.toString(relation), EdgeEventType.RELATION, EdgeUtils.getEdgeEventActionTypeByActionType(event.getActionType()),
edgeSynchronizationManager.getEdgeId().get());
} catch (Exception e) {
log.error("[{}] failed to process RelationActionEvent: {}", event.getTenantId(), event, e);
@@ -173,4 +185,25 @@ public class EdgeEventSourcingListener {
}
}
}
+
+ private EdgeEventType getEdgeEventTypeForEntityEvent(Object entity) {
+ if (entity instanceof AlarmComment) {
+ return EdgeEventType.ALARM_COMMENT;
+ }
+ return null;
+ }
+
+ private String getBodyMsgForEntityEvent(Object entity) {
+ if (entity instanceof AlarmComment) {
+ return JacksonUtil.toString(entity);
+ }
+ return null;
+ }
+
+ private EdgeEventActionType getActionForEntityEvent(Object entity, boolean isCreated) {
+ if (entity instanceof AlarmComment) {
+ return isCreated ? EdgeEventActionType.ADDED_COMMENT : EdgeEventActionType.UPDATED_COMMENT;
+ }
+ return isCreated ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED;
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
index f1f473f976..5e6f7a263e 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
@@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
@@ -530,6 +531,9 @@ public final class EdgeGrpcSession implements Closeable {
case RPC_CALL:
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
+ case ADDED_COMMENT:
+ case UPDATED_COMMENT:
+ case DELETED_COMMENT:
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg);
break;
@@ -644,6 +648,8 @@ public final class EdgeGrpcSession implements Closeable {
return ctx.getRuleChainProcessor().convertRuleChainMetadataEventToDownlink(edgeEvent, this.edgeVersion);
case ALARM:
return ctx.getAlarmProcessor().convertAlarmEventToDownlink(edgeEvent, this.edgeVersion);
+ case ALARM_COMMENT:
+ return ctx.getAlarmProcessor().convertAlarmCommentEventToDownlink(edgeEvent, this.edgeVersion);
case USER:
return ctx.getUserProcessor().convertUserEventToDownlink(edgeEvent, this.edgeVersion);
case RELATION:
@@ -714,6 +720,12 @@ public final class EdgeGrpcSession implements Closeable {
.processAlarmMsgFromEdge(edge.getTenantId(), edge.getId(), alarmUpdateMsg));
}
}
+ if (uplinkMsg.getAlarmCommentUpdateMsgCount() > 0) {
+ for (AlarmCommentUpdateMsg alarmCommentUpdateMsg : uplinkMsg.getAlarmCommentUpdateMsgList()) {
+ result.add(((AlarmProcessor) ctx.getAlarmEdgeProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
+ .processAlarmCommentMsgFromEdge(edge.getTenantId(), edge.getId(), alarmCommentUpdateMsg));
+ }
+ }
if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) {
for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) {
result.add(((EntityViewProcessor) ctx.getEntityViewProcessorFactory().getProcessorByEdgeVersion(this.edgeVersion))
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java
index 81606434b5..b5a0f20d6b 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructor.java
@@ -16,6 +16,8 @@
package org.thingsboard.server.service.edge.rpc.constructor.alarm;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor;
@@ -23,4 +25,6 @@ import org.thingsboard.server.service.edge.rpc.constructor.MsgConstructor;
public interface AlarmMsgConstructor extends MsgConstructor {
AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName);
+
+ AlarmCommentUpdateMsg constructAlarmCommentUpdatedMsg(UpdateMsgType msgType, AlarmComment alarmComment);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java
index 5abba029de..81a489671f 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV1.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@TbCoreComponent
-public class AlarmMsgConstructorV1 implements AlarmMsgConstructor {
+public class AlarmMsgConstructorV1 extends BaseAlarmMsgConstructor {
@Override
public AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName) {
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java
index d9768b8f9e..143a41b73c 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/AlarmMsgConstructorV2.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@TbCoreComponent
-public class AlarmMsgConstructorV2 implements AlarmMsgConstructor {
+public class AlarmMsgConstructorV2 extends BaseAlarmMsgConstructor {
@Override
public AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm, String entityName) {
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/BaseAlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/BaseAlarmMsgConstructor.java
new file mode 100644
index 0000000000..c9af86e8cc
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/alarm/BaseAlarmMsgConstructor.java
@@ -0,0 +1,29 @@
+/**
+ * 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.edge.rpc.constructor.alarm;
+
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
+
+public abstract class BaseAlarmMsgConstructor implements AlarmMsgConstructor {
+
+ @Override
+ public AlarmCommentUpdateMsg constructAlarmCommentUpdatedMsg(UpdateMsgType msgType, AlarmComment alarmComment) {
+ return AlarmCommentUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(alarmComment)).build();
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
index 6f7474b15d..256f191215 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
@@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@@ -145,6 +147,9 @@ public abstract class BaseEdgeProcessor {
@Autowired
protected AlarmService alarmService;
+ @Autowired
+ protected AlarmCommentService alarmCommentService;
+
@Autowired
protected DeviceService deviceService;
@@ -350,10 +355,13 @@ public abstract class BaseEdgeProcessor {
case ALARM_ASSIGNED:
case ALARM_UNASSIGNED:
case CREDENTIALS_REQUEST:
+ case ADDED_COMMENT:
+ case UPDATED_COMMENT:
return true;
}
switch (type) {
case ALARM:
+ case ALARM_COMMENT:
case RULE_CHAIN:
case RULE_CHAIN_METADATA:
case USER:
@@ -441,14 +449,17 @@ public abstract class BaseEdgeProcessor {
case CREDENTIALS_UPDATED:
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
+ case UPDATED_COMMENT:
return UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE;
case ADDED:
case ASSIGNED_TO_EDGE:
case RELATION_ADD_OR_UPDATE:
+ case ADDED_COMMENT:
return UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE;
case DELETED:
case UNASSIGNED_FROM_EDGE:
case RELATION_DELETED:
+ case DELETED_COMMENT:
return UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE;
case ALARM_ACK:
return UpdateMsgType.ALARM_ACK_RPC_MESSAGE;
@@ -517,22 +528,14 @@ public abstract class BaseEdgeProcessor {
private ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type,
EdgeEventActionType actionType, EdgeId sourceEdgeId) {
- PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
- PageData pageData;
List> futures = new ArrayList<>();
- do {
- pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId, pageLink);
- if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) {
- for (EdgeId relatedEdgeId : pageData.getData()) {
- if (!relatedEdgeId.equals(sourceEdgeId)) {
- futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null));
- }
- }
- if (pageData.hasNext()) {
- pageLink = pageLink.nextPageLink();
- }
+ PageDataIterableByTenantIdEntityId edgeIds =
+ new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE);
+ for (EdgeId relatedEdgeId : edgeIds) {
+ if (!relatedEdgeId.equals(sourceEdgeId)) {
+ futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null));
}
- } while (pageData != null && pageData.hasNext());
+ }
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
}
@@ -715,19 +718,13 @@ public abstract class BaseEdgeProcessor {
}
private boolean isEntityNotAssignedToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) {
- PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
- PageData pageData;
- do {
- pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId, pageLink);
- if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) {
- if (pageData.getData().contains(edgeId)) {
- return false;
- }
- if (pageData.hasNext()) {
- pageLink = pageLink.nextPageLink();
- }
+ PageDataIterableByTenantIdEntityId edgeIds =
+ new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE);
+ for (EdgeId edgeId1 : edgeIds) {
+ if (edgeId1.equals(edgeId)) {
+ return false;
}
- } while (pageData != null && pageData.hasNext());
+ }
return true;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
index ea48032b69..f83cbbba9c 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
@@ -29,12 +30,14 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.page.PageData;
-import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
+import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.service.edge.rpc.constructor.alarm.AlarmMsgConstructor;
import java.util.ArrayList;
import java.util.List;
@@ -67,58 +70,90 @@ public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements A
return null;
}
+ @Override
+ public ListenableFuture processAlarmCommentMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmCommentUpdateMsg alarmCommentUpdateMsg) {
+ log.trace("[{}] processAlarmCommentMsgFromEdge [{}]", tenantId, alarmCommentUpdateMsg);
+ try {
+ edgeSynchronizationManager.getEdgeId().set(edgeId);
+ return processAlarmCommentMsg(tenantId, alarmCommentUpdateMsg);
+ } finally {
+ edgeSynchronizationManager.getEdgeId().remove();
+ }
+ }
+
+ @Override
+ public DownlinkMsg convertAlarmCommentEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
+ UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
+ switch (edgeEvent.getAction()) {
+ case ADDED_COMMENT:
+ case UPDATED_COMMENT:
+ case DELETED_COMMENT:
+ AlarmComment alarmComment = JacksonUtil.convertValue(edgeEvent.getBody(), AlarmComment.class);
+ if (alarmComment != null) {
+ return DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addAlarmCommentUpdateMsg(((AlarmMsgConstructor) alarmMsgConstructorFactory
+ .getMsgConstructorByEdgeVersion(edgeVersion)).constructAlarmCommentUpdatedMsg(msgType, alarmComment))
+ .build();
+ }
+ default:
+ return null;
+ }
+ }
+
public ListenableFuture processAlarmNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
- switch (actionType) {
- case DELETED:
- Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class);
- if (deletedAlarm == null) {
- return Futures.immediateFuture(null);
- }
- List> delFutures = pushEventToAllRelatedEdges(tenantId, deletedAlarm.getOriginator(),
- alarmId, actionType, JacksonUtil.valueToTree(deletedAlarm), originatorEdgeId);
- return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService);
- default:
- ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId);
- return Futures.transformAsync(alarmFuture, alarm -> {
- if (alarm == null) {
- return Futures.immediateFuture(null);
- }
- EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(alarm.getOriginator().getEntityType());
- if (type == null) {
- return Futures.immediateFuture(null);
- }
- List> futures = pushEventToAllRelatedEdges(tenantId, alarm.getOriginator(),
- alarmId, actionType, null, originatorEdgeId);
- return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
- }, dbCallbackExecutorService);
+ if (EdgeEventActionType.DELETED.equals(actionType)) {
+ Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class);
+ if (deletedAlarm == null) {
+ return Futures.immediateFuture(null);
+ }
+ List> delFutures = pushEventToAllRelatedEdges(tenantId, deletedAlarm.getOriginator(),
+ alarmId, actionType, JacksonUtil.valueToTree(deletedAlarm), originatorEdgeId, EdgeEventType.ALARM);
+ return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService);
+ }
+ ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId);
+ return Futures.transformAsync(alarmFuture, alarm -> {
+ if (alarm == null) {
+ return Futures.immediateFuture(null);
+ }
+ EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(alarm.getOriginator().getEntityType());
+ if (type == null) {
+ return Futures.immediateFuture(null);
+ }
+ List> futures = pushEventToAllRelatedEdges(tenantId, alarm.getOriginator(),
+ alarmId, actionType, null, originatorEdgeId, EdgeEventType.ALARM);
+ return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
+ }, dbCallbackExecutorService);
+ }
+
+ public ListenableFuture processAlarmCommentNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
+ EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
+ AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
+ EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
+ AlarmComment alarmComment = JacksonUtil.fromString(edgeNotificationMsg.getBody(), AlarmComment.class);
+ if (alarmComment == null) {
+ return Futures.immediateFuture(null);
}
+ Alarm alarmById = alarmService.findAlarmById(tenantId, new AlarmId(alarmComment.getAlarmId().getId()));
+ List> delFutures = pushEventToAllRelatedEdges(tenantId, alarmById.getOriginator(),
+ alarmId, actionType, JacksonUtil.valueToTree(alarmComment), originatorEdgeId, EdgeEventType.ALARM_COMMENT);
+ return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService);
}
- private List> pushEventToAllRelatedEdges(TenantId tenantId, EntityId originatorId, AlarmId alarmId, EdgeEventActionType actionType, JsonNode body, EdgeId sourceEdgeId) {
- PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
- PageData pageData;
+ private List> pushEventToAllRelatedEdges(TenantId tenantId, EntityId originatorId, AlarmId alarmId,
+ EdgeEventActionType actionType, JsonNode body, EdgeId sourceEdgeId,
+ EdgeEventType edgeEventType) {
List> futures = new ArrayList<>();
- do {
- pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, originatorId, pageLink);
- if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) {
- for (EdgeId relatedEdgeId : pageData.getData()) {
- if (!relatedEdgeId.equals(sourceEdgeId)) {
- futures.add(saveEdgeEvent(tenantId,
- relatedEdgeId,
- EdgeEventType.ALARM,
- actionType,
- alarmId,
- body));
- }
- }
- if (pageData.hasNext()) {
- pageLink = pageLink.nextPageLink();
- }
+ PageDataIterableByTenantIdEntityId edgeIds =
+ new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, originatorId, DEFAULT_PAGE_SIZE);
+ for (EdgeId relatedEdgeId : edgeIds) {
+ if (!relatedEdgeId.equals(sourceEdgeId)) {
+ futures.add(saveEdgeEvent(tenantId, relatedEdgeId, edgeEventType, actionType, alarmId, body));
}
- } while (pageData != null && pageData.hasNext());
+ }
return futures;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java
index 68a72bf01e..938269462d 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmProcessor.java
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@@ -29,4 +30,8 @@ public interface AlarmProcessor extends EdgeProcessor {
ListenableFuture processAlarmMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmUpdateMsg alarmUpdateMsg);
DownlinkMsg convertAlarmEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion);
+
+ ListenableFuture processAlarmCommentMsgFromEdge(TenantId tenantId, EdgeId edgeId, AlarmCommentUpdateMsg alarmCommentUpdateMsg);
+
+ DownlinkMsg convertAlarmCommentEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java
index 546d6e63db..97e1590b5b 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java
@@ -19,10 +19,12 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.asset.Asset;
@@ -33,6 +35,8 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.alarm.AlarmCommentDao;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@@ -44,6 +48,9 @@ import java.util.UUID;
@Slf4j
public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
+ @Autowired
+ protected AlarmCommentDao alarmCommentDao;
+
public ListenableFuture processAlarmMsg(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) {
log.trace("[{}] processAlarmMsg [{}]", tenantId, alarmUpdateMsg);
AlarmId alarmId = new AlarmId(new UUID(alarmUpdateMsg.getIdMSB(), alarmUpdateMsg.getIdLSB()));
@@ -93,6 +100,42 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
return Futures.immediateFuture(null);
}
+ public ListenableFuture processAlarmCommentMsg(TenantId tenantId, AlarmCommentUpdateMsg alarmCommentUpdateMsg) {
+ log.trace("[{}] processAlarmCommentMsg [{}]", tenantId, alarmCommentUpdateMsg);
+ AlarmComment alarmComment = JacksonUtil.fromString(alarmCommentUpdateMsg.getEntity(), AlarmComment.class, true);
+ if (alarmComment == null) {
+ throw new RuntimeException("[{" + tenantId + "}] alarmCommentUpdateMsg {" + alarmCommentUpdateMsg + "} cannot be converted to alarm comment");
+ }
+ try {
+ Alarm alarm = alarmService.findAlarmById(tenantId, new AlarmId(alarmComment.getAlarmId().getId()));
+ if (alarm == null) {
+ return Futures.immediateFuture(null);
+ }
+ switch (alarmCommentUpdateMsg.getMsgType()) {
+ case ENTITY_CREATED_RPC_MESSAGE:
+ alarmCommentDao.save(tenantId, alarmComment);
+ break;
+ case ENTITY_UPDATED_RPC_MESSAGE:
+ alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment);
+ break;
+ case ENTITY_DELETED_RPC_MESSAGE:
+ AlarmComment alarmCommentToDelete = alarmCommentService.findAlarmCommentById(tenantId, alarmComment.getId());
+ if (alarmCommentToDelete != null) {
+ alarmCommentService.saveAlarmComment(tenantId, alarmCommentToDelete);
+ }
+ break;
+ case UNRECOGNIZED:
+ default:
+ return handleUnsupportedMsgType(alarmCommentUpdateMsg.getMsgType());
+ }
+ } catch (Exception e) {
+ log.error("[{}] Failed to process alarm comment update msg [{}]", tenantId, alarmCommentUpdateMsg, e);
+ return Futures.immediateFailedFuture(e);
+ }
+ return Futures.immediateFuture(null);
+ }
+
+
protected abstract EntityId getAlarmOriginatorFromMsg(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg);
protected abstract Alarm constructAlarmFromUpdateMsg(TenantId tenantId, AlarmId alarmId, EntityId originatorId, AlarmUpdateMsg alarmUpdateMsg);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
index 321cc69604..3a87fff506 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
@@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
-import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.HasName;
@@ -27,7 +26,6 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
-import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
@@ -40,6 +38,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
@@ -174,39 +173,4 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
metaData.putValue("assignedFromTenantName", tenant.getName());
return metaData;
}
-
- public static EdgeEventActionType edgeTypeByActionType(ActionType actionType) {
- switch (actionType) {
- case ADDED:
- return EdgeEventActionType.ADDED;
- case UPDATED:
- return EdgeEventActionType.UPDATED;
- case ALARM_ACK:
- return EdgeEventActionType.ALARM_ACK;
- case ALARM_CLEAR:
- return EdgeEventActionType.ALARM_CLEAR;
- case ALARM_ASSIGNED:
- return EdgeEventActionType.ALARM_ASSIGNED;
- case ALARM_UNASSIGNED:
- return EdgeEventActionType.ALARM_UNASSIGNED;
- case DELETED:
- return EdgeEventActionType.DELETED;
- case RELATION_ADD_OR_UPDATE:
- return EdgeEventActionType.RELATION_ADD_OR_UPDATE;
- case RELATION_DELETED:
- return EdgeEventActionType.RELATION_DELETED;
- case ASSIGNED_TO_CUSTOMER:
- return EdgeEventActionType.ASSIGNED_TO_CUSTOMER;
- case UNASSIGNED_FROM_CUSTOMER:
- return EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER;
- case ASSIGNED_TO_EDGE:
- return EdgeEventActionType.ASSIGNED_TO_EDGE;
- case UNASSIGNED_FROM_EDGE:
- return EdgeEventActionType.UNASSIGNED_FROM_EDGE;
- case CREDENTIALS_UPDATED:
- return EdgeEventActionType.CREDENTIALS_UPDATED;
- default:
- return null;
- }
- }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java
index c0274792cb..da7c69050e 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java
@@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.exception.ThingsboardException;
public interface TbAlarmCommentService {
+
AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException;
void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 9ef51c9b00..ddc848a91d 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -85,6 +85,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
@@ -169,6 +170,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private TimeseriesService tsService;
+ @Autowired
+ private DeviceConnectivityConfiguration connectivityConfiguration;
+
@Value("${state.persistToTelemetry:false}")
@Getter
private boolean persistActivityToTelemetry;
@@ -260,7 +264,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
ObjectNode node = JacksonUtil.newObjectNode();
node.put("baseUrl", "http://localhost:8080");
node.put("prohibitDifferentUrl", false);
- node.set("connectivity", createDeviceConnectivityConfiguration());
generalSettings.setJsonValue(node);
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
@@ -285,51 +288,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AdminSettings connectivitySettings = new AdminSettings();
connectivitySettings.setTenantId(TenantId.SYS_TENANT_ID);
connectivitySettings.setKey("connectivity");
- connectivitySettings.setJsonValue(createDeviceConnectivityConfiguration());
+ connectivitySettings.setJsonValue(JacksonUtil.valueToTree(connectivityConfiguration.getConnectivity()));
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, connectivitySettings);
}
- private ObjectNode createDeviceConnectivityConfiguration() {
- ObjectNode config = JacksonUtil.newObjectNode();
-
- ObjectNode http = JacksonUtil.newObjectNode();
- http.put("enabled", true);
- http.put("host", "");
- http.put("port", 8080);
- config.set("http", http);
-
- ObjectNode https = JacksonUtil.newObjectNode();
- https.put("enabled", false);
- https.put("host", "");
- https.put("port", 443);
- config.set("https", https);
-
- ObjectNode mqtt = JacksonUtil.newObjectNode();
- mqtt.put("enabled", true);
- mqtt.put("host", "");
- mqtt.put("port", 1883);
- config.set("mqtt", mqtt);
-
- ObjectNode mqtts = JacksonUtil.newObjectNode();
- mqtts.put("enabled", false);
- mqtts.put("host", "");
- mqtts.put("port", 8883);
- config.set("mqtts", mqtts);
-
- ObjectNode coap = JacksonUtil.newObjectNode();
- coap.put("enabled", true);
- coap.put("host", "");
- coap.put("port", 5683);
- config.set("coap", coap);
-
- ObjectNode coaps = JacksonUtil.newObjectNode();
- coaps.put("enabled", false);
- coaps.put("host", "");
- coaps.put("port", 5684);
- config.set("coaps", coaps);
- return config;
- }
-
@Override
public void createRandomJwtSettings() throws Exception {
jwtSettingsService.createRandomJwtSettings();
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index 9169a37515..969e310e5b 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -181,7 +181,7 @@ public class DefaultMailService implements MailService {
passwordResetExecutorService.execute(() -> {
try {
this.sendResetPasswordEmail(passwordResetLink, email);
- } catch (ThingsboardException e) {
+ } catch (Exception e) {
log.error("Error occurred: {} ", e.getMessage());
}
});
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
index b7847b9857..e1db043188 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
@@ -19,10 +19,10 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
+import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
-import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
@@ -48,6 +48,9 @@ public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerPro
return false;
}
long currentCount = entityCountService.countByTenantIdAndEntityType(trigger.getTenantId(), trigger.getEntityType());
+ if (currentCount == 0) {
+ return false;
+ }
trigger.setLimit(limit);
trigger.setCurrentCount(currentCount);
return (int) (limit * triggerConfig.getThreshold()) == currentCount; // strict comparing not to send notification on each new entity
@@ -59,7 +62,7 @@ public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerPro
.entityType(trigger.getEntityType())
.currentCount(trigger.getCurrentCount())
.limit(trigger.getLimit())
- .percents((int) (((float)trigger.getCurrentCount() / trigger.getLimit()) * 100))
+ .percents((int) (((float) trigger.getCurrentCount() / trigger.getLimit()) * 100))
.tenantId(trigger.getTenantId())
.tenantName(tenantService.findTenantById(trigger.getTenantId()).getName())
.build();
diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
index 0453a26ff6..2865e4a01a 100644
--- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
@@ -225,7 +225,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Override
public DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
- boolean isTimeseries, boolean isAttributes) {
+ boolean isTimeseries, boolean isAttributes, String attributesScope) {
final DeferredResult response = new DeferredResult<>();
if (!isAttributes && !isTimeseries) {
replyWithEmptyResponse(response);
@@ -253,7 +253,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
if (isAttributes) {
Map> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType));
List>> futures = new ArrayList<>(typesMap.size());
- typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, type, entityIds))));
+ typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, type, entityIds, attributesScope))));
attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> {
if (CollectionUtils.isEmpty(lists)) {
return Collections.emptyList();
diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
index 0ebc1b43f4..fd0f05df2f 100644
--- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
@@ -38,6 +38,6 @@ public interface EntityQueryService {
long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query);
DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
- boolean isTimeseries, boolean isAttributes);
+ boolean isTimeseries, boolean isAttributes, String attributesScope);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
index b88674c8ce..ee05e82900 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
@@ -44,6 +44,8 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.TbMsg;
@@ -57,6 +59,7 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg;
import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
+import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
@@ -68,8 +71,8 @@ import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
-import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
@@ -77,6 +80,7 @@ import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
@@ -116,6 +120,7 @@ public class DefaultTbClusterService implements TbClusterService {
private final TbDeviceProfileCache deviceProfileCache;
private final TbAssetProfileCache assetProfileCache;
private final GatewayNotificationsService gatewayNotificationsService;
+ private final EdgeService edgeService;
@Override
public void pushMsgToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, TbQueueCallback callback) {
@@ -546,11 +551,17 @@ public class DefaultTbClusterService implements TbClusterService {
pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), edgeId), null);
break;
case UNASSIGNED_FROM_EDGE:
- pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), null), null);
+ EdgeId relatedEdgeId = findRelatedEdgeIdIfAny(tenantId, entityId);
+ pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), relatedEdgeId), null);
break;
}
}
+ private EdgeId findRelatedEdgeIdIfAny(TenantId tenantId, EntityId entityId) {
+ PageData pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId, new PageLink(1));
+ return Optional.ofNullable(pageData).filter(pd -> pd.getTotalElements() > 0).map(pd -> pd.getData().get(0)).orElse(null);
+ }
+
@Override
public void onQueueChange(Queue queue) {
log.trace("[{}][{}] Processing queue change [{}] event", queue.getTenantId(), queue.getId(), queue.getName());
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index e888dcd526..2d5e6963eb 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -373,10 +373,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService 0 ? RpcError.values()[proto.getError()] : null;
@@ -164,10 +165,10 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
tbDeviceRpcService.processRpcResponseFromDevice(response);
callback.onSuccess();
} else if (nfMsg.hasQueueUpdateMsg()) {
- ctx.getScheduler().execute(() -> updateQueue(nfMsg.getQueueUpdateMsg()));
+ updateQueue(nfMsg.getQueueUpdateMsg());
callback.onSuccess();
} else if (nfMsg.hasQueueDeleteMsg()) {
- ctx.getScheduler().execute(() -> deleteQueue(nfMsg.getQueueDeleteMsg()));
+ deleteQueue(nfMsg.getQueueDeleteMsg());
callback.onSuccess();
} else {
log.trace("Received notification with missing handler");
@@ -204,13 +205,30 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId);
var consumerManager = consumers.remove(queueKey);
if (consumerManager != null) {
- consumerManager.delete();
+ consumerManager.delete(true);
}
partitionService.removeQueue(queueDeleteMsg);
partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE)));
}
+ @EventListener
+ public void handleComponentLifecycleEvent(ComponentLifecycleMsg event) {
+ if (event.getEntityId().getEntityType() == EntityType.TENANT) {
+ if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
+ List toRemove = consumers.keySet().stream()
+ .filter(queueKey -> queueKey.getTenantId().equals(event.getTenantId()))
+ .collect(Collectors.toList());
+ toRemove.forEach(queueKey -> {
+ var consumerManager = consumers.remove(queueKey);
+ if (consumerManager != null) {
+ consumerManager.delete(false);
+ }
+ });
+ }
+ }
+ }
+
private TbRuleEngineQueueConsumerManager getOrCreateConsumer(QueueKey queueKey) {
return consumers.computeIfAbsent(queueKey, key -> new TbRuleEngineQueueConsumerManager(ctx, key));
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
index 394f41883d..3065652d0f 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.queue.processing;
-import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
@@ -30,7 +29,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
@@ -166,57 +164,51 @@ public abstract class AbstractConsumerService actorMsgOpt = encodingService.decode(nfMsg.toByteArray());
- actorMsgOpt.ifPresent(tbActorMsg -> handleComponentLifecycleMsg(id, tbActorMsg));
- }
-
- protected void handleComponentLifecycleMsg(UUID id, TbActorMsg actorMsg) {
- if (actorMsg instanceof ComponentLifecycleMsg) {
- ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg;
- log.debug("[{}][{}][{}] Received Lifecycle event: {}", componentLifecycleMsg.getTenantId(), componentLifecycleMsg.getEntityId().getEntityType(),
- componentLifecycleMsg.getEntityId(), componentLifecycleMsg.getEvent());
- if (EntityType.TENANT_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- TenantProfileId tenantProfileId = new TenantProfileId(componentLifecycleMsg.getEntityId().getId());
- tenantProfileCache.evict(tenantProfileId);
+ protected final void handleComponentLifecycleMsg(UUID id, ComponentLifecycleMsg componentLifecycleMsg) {
+ TenantId tenantId = componentLifecycleMsg.getTenantId();
+ log.debug("[{}][{}][{}] Received Lifecycle event: {}", tenantId, componentLifecycleMsg.getEntityId().getEntityType(),
+ componentLifecycleMsg.getEntityId(), componentLifecycleMsg.getEvent());
+ if (EntityType.TENANT_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ TenantProfileId tenantProfileId = new TenantProfileId(componentLifecycleMsg.getEntityId().getId());
+ tenantProfileCache.evict(tenantProfileId);
+ if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
+ apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
+ }
+ } else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
+ jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
+ return;
+ } else {
+ tenantProfileCache.evict(tenantId);
+ partitionService.evictTenantInfo(tenantId);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
- apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
- }
- } else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- if (TenantId.SYS_TENANT_ID.equals(componentLifecycleMsg.getTenantId())) {
- jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
- return;
- } else {
- tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
- partitionService.removeTenant(componentLifecycleMsg.getTenantId());
- if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
- apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
- } else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
- apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
- }
- }
- } else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
- } else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceId(componentLifecycleMsg.getEntityId().getId()));
- } else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- assetProfileCache.evict(componentLifecycleMsg.getTenantId(), new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
- } else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- assetProfileCache.evict(componentLifecycleMsg.getTenantId(), new AssetId(componentLifecycleMsg.getEntityId().getId()));
- } else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
- } else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- apiUsageStateService.onApiUsageStateUpdate(componentLifecycleMsg.getTenantId());
- } else if (EntityType.CUSTOMER.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
- if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
- apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
+ apiUsageStateService.onTenantUpdate(tenantId);
+ } else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
+ apiUsageStateService.onTenantDelete(tenantId);
+ partitionService.removeTenant(tenantId);
}
}
- eventPublisher.publishEvent(componentLifecycleMsg);
+ } else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ deviceProfileCache.evict(tenantId, new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
+ } else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ deviceProfileCache.evict(tenantId, new DeviceId(componentLifecycleMsg.getEntityId().getId()));
+ } else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ assetProfileCache.evict(tenantId, new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
+ } else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ assetProfileCache.evict(tenantId, new AssetId(componentLifecycleMsg.getEntityId().getId()));
+ } else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
+ } else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ apiUsageStateService.onApiUsageStateUpdate(tenantId);
+ } else if (EntityType.CUSTOMER.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
+ apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
+ }
}
- log.trace("[{}] Forwarding component lifecycle message to App Actor {}", id, actorMsg);
- actorContext.tellWithHighPriority(actorMsg);
+
+ eventPublisher.publishEvent(componentLifecycleMsg);
+ log.trace("[{}] Forwarding component lifecycle message to App Actor {}", id, componentLifecycleMsg);
+ actorContext.tellWithHighPriority(componentLifecycleMsg);
}
protected abstract void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception;
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java
index 20eea32a6a..3c52e31afe 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.queue.ruleengine;
+import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.queue.Queue;
@@ -24,24 +25,24 @@ import java.util.Set;
@Getter
@ToString
+@AllArgsConstructor
public class TbQueueConsumerManagerTask {
private final QueueEvent event;
private Queue queue;
private Set partitions;
+ private boolean drainQueue;
- public TbQueueConsumerManagerTask(QueueEvent event) {
- this.event = event;
+ public static TbQueueConsumerManagerTask delete(boolean drainQueue) {
+ return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue);
}
- public TbQueueConsumerManagerTask(QueueEvent event, Queue queue) {
- this.event = event;
- this.queue = queue;
+ public static TbQueueConsumerManagerTask configUpdate(Queue queue) {
+ return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, queue, null, false);
}
- public TbQueueConsumerManagerTask(QueueEvent event, Set partitions) {
- this.event = event;
- this.partitions = partitions;
+ public static TbQueueConsumerManagerTask partitionChange(Set partitions) {
+ return new TbQueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, null, partitions, false);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
index 22db5b7b38..2da3dbc6dc 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
@@ -95,15 +95,15 @@ public class TbRuleEngineQueueConsumerManager {
}
public void update(Queue queue) {
- addTask(new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, queue));
+ addTask(TbQueueConsumerManagerTask.configUpdate(queue));
}
public void update(Set partitions) {
- addTask(new TbQueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, partitions));
+ addTask(TbQueueConsumerManagerTask.partitionChange(partitions));
}
- public void delete() {
- addTask(new TbQueueConsumerManagerTask(QueueEvent.DELETE));
+ public void delete(boolean drainQueue) {
+ addTask(TbQueueConsumerManagerTask.delete(drainQueue));
}
private void addTask(TbQueueConsumerManagerTask todo) {
@@ -138,7 +138,7 @@ public class TbRuleEngineQueueConsumerManager {
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) {
newConfiguration = task.getQueue();
} else if (task.getEvent() == QueueEvent.DELETE) {
- doDelete();
+ doDelete(task.isDrainQueue());
return;
}
}
@@ -205,7 +205,7 @@ public class TbRuleEngineQueueConsumerManager {
log.debug("[{}] Unsubscribed and stopped consumers", queueKey);
}
- private void doDelete() {
+ private void doDelete(boolean drainQueue) {
stopped = true;
log.info("[{}] Handling queue deletion", queueKey);
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
@@ -213,7 +213,9 @@ public class TbRuleEngineQueueConsumerManager {
List>> queueConsumers = consumerWrapper.getConsumers().stream()
.map(TbQueueConsumerTask::getConsumer).collect(Collectors.toList());
ctx.getConsumersExecutor().submit(() -> {
- drainQueue(queueConsumers);
+ if (drainQueue) {
+ drainQueue(queueConsumers);
+ }
queueConsumers.forEach(consumer -> {
for (String topic : consumer.getFullTopicNames()) {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
index f52fc32f4f..d0e3444d0f 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
@@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
@@ -47,12 +46,12 @@ import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.entitiy.tenant.TbTenantService;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
-import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
@@ -71,6 +70,9 @@ public abstract class AbstractOAuth2ClientMapper {
@Autowired
private TenantService tenantService;
+ @Autowired
+ private TbTenantService tbTenantService;
+
@Autowired
private CustomerService customerService;
@@ -92,7 +94,7 @@ public abstract class AbstractOAuth2ClientMapper {
@Value("${edges.enabled}")
@Getter
private boolean edgesEnabled;
-
+
private final Lock userCreationLock = new ReentrantLock();
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2Registration registration) {
@@ -171,19 +173,13 @@ public abstract class AbstractOAuth2ClientMapper {
}
}
- private TenantId getTenantId(String tenantName) throws IOException {
+ private TenantId getTenantId(String tenantName) throws Exception {
List tenants = tenantService.findTenants(new PageLink(1, 0, tenantName)).getData();
Tenant tenant;
if (tenants == null || tenants.isEmpty()) {
tenant = new Tenant();
tenant.setTitle(tenantName);
- tenant = tenantService.saveTenant(tenant);
- installScripts.createDefaultRuleChains(tenant.getId());
- installScripts.createDefaultEdgeRuleChains(tenant.getId());
- tenantProfileCache.evict(tenant.getId());
- tbClusterService.onTenantChange(tenant, null);
- tbClusterService.broadcastEntityStateChangeEvent(tenant.getId(), tenant.getId(),
- ComponentLifecycleEvent.CREATED);
+ tenant = tbTenantService.save(tenant);
} else {
tenant = tenants.get(0);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
index 37070718a3..e010cf3050 100644
--- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -185,6 +185,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService deviceStates = new ConcurrentHashMap<>();
@@ -803,7 +807,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, key, value));
+ telemetryTtl, new TelemetrySaveCallback<>(deviceId, key, value));
} else {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value));
}
@@ -814,7 +818,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, key, value));
+ telemetryTtl, new TelemetrySaveCallback<>(deviceId, key, value));
} else {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value));
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
index 96bdbd5561..5dc6b0e636 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
@@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
@@ -37,15 +36,16 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
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.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
-import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
-import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
+import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.event.OtherServiceShutdownEvent;
+import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
@@ -154,7 +154,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
entitySubscriptions.values().removeIf(sub ->
- !partitionService.resolve(ServiceType.TB_CORE, sub.getTenantId(), sub.getEntityId()).isMyPartition());
+ !partitionService.isMyPartition(ServiceType.TB_CORE, sub.getTenantId(), sub.getEntityId()));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
index 5251ee72f6..6e31a24275 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
@@ -560,17 +560,14 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
List keys = cmd.getKeys();
List finalTsKvQueryList;
List tsKvQueryList = keys.stream().map(key -> {
- var query = new BaseReadTsKvQuery(
- key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg()
- );
+ var query = new BaseReadTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.toAggregationParams(), getLimit(cmd.getLimit()));
queriesKeys.put(query.getId(), query.getKey());
return query;
}).collect(Collectors.toList());
if (cmd.isFetchLatestPreviousPoint()) {
finalTsKvQueryList = new ArrayList<>(tsKvQueryList);
finalTsKvQueryList.addAll(keys.stream().map(key -> {
- var query = new BaseReadTsKvQuery(
- key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg());
+ var query = new BaseReadTsKvQuery(key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.toAggregationParams(), 1);
queriesKeys.put(query.getId(), query.getKey());
return query;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
index bc156c3d01..bdeb93c329 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
@@ -21,7 +21,6 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
-import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java
index 6ae9a03ff8..3b3dd0d55d 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import lombok.Data;
import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.IntervalType;
import java.util.List;
@@ -26,7 +27,9 @@ public class EntityHistoryCmd implements GetTsCmd {
private List keys;
private long startTs;
private long endTs;
+ private IntervalType intervalType;
private long interval;
+ private String timeZoneId;
private int limit;
private Aggregation agg;
private boolean fetchLatestPreviousPoint;
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java
index 039873bb18..ef5d9efb96 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java
@@ -16,6 +16,8 @@
package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AggregationParams;
+import org.thingsboard.server.common.data.kv.IntervalType;
import java.util.List;
@@ -27,12 +29,28 @@ public interface GetTsCmd {
List getKeys();
+ IntervalType getIntervalType();
+
long getInterval();
+ String getTimeZoneId();
+
int getLimit();
Aggregation getAgg();
boolean isFetchLatestPreviousPoint();
+ default AggregationParams toAggregationParams() {
+ var agg = getAgg();
+ var intervalType = getIntervalType();
+ if (agg == null || Aggregation.NONE.equals(agg)) {
+ return AggregationParams.none();
+ } else if (intervalType == null || IntervalType.MILLISECONDS.equals(intervalType)) {
+ return AggregationParams.milliseconds(agg, getInterval());
+ } else {
+ return AggregationParams.calendar(agg, intervalType, getTimeZoneId());
+ }
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java
index e7a9e03d17..3c3d53e426 100644
--- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.IntervalType;
import java.util.List;
@@ -27,7 +28,9 @@ public class TimeSeriesCmd implements GetTsCmd {
private List keys;
private long startTs;
private long timeWindow;
+ private IntervalType intervalType;
private long interval;
+ private String timeZoneId;
private int limit;
private Aggregation agg;
private boolean fetchLatestPreviousPoint;
diff --git a/application/src/main/java/org/thingsboard/server/utils/TbNodeUpgradeUtils.java b/application/src/main/java/org/thingsboard/server/utils/TbNodeUpgradeUtils.java
index 157bbbe816..c1e7473434 100644
--- a/application/src/main/java/org/thingsboard/server/utils/TbNodeUpgradeUtils.java
+++ b/application/src/main/java/org/thingsboard/server/utils/TbNodeUpgradeUtils.java
@@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
+import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
+
@Slf4j
public class TbNodeUpgradeUtils {
@@ -44,9 +46,13 @@ public class TbNodeUpgradeUtils {
} else {
var tbVersionedNode = getTbVersionedNode(nodeInfo);
try {
+ JsonNode queueName = oldConfiguration.get(QUEUE_NAME);
TbPair upgradeResult = tbVersionedNode.upgrade(configurationVersion, oldConfiguration);
if (upgradeResult.getFirst()) {
node.setConfiguration(upgradeResult.getSecond());
+ if (nodeInfo.getAnnotation().hasQueueName() && queueName != null && queueName.isTextual()) {
+ node.setQueueName(queueName.asText());
+ }
}
} catch (Exception e) {
try {
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index e746dc9dba..936f30af60 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -777,7 +777,12 @@ audit-log:
# Device state parameters
state:
- # Should be greater than transport.sessions.report_timeout
+ # Device inactivity timeout is a global configuration parameter that defines when the device will be marked as "inactive" by the server.
+ # The parameter value is in seconds. A user can overwrite this parameter for an individual device by setting the “inactivityTimeout” server-side attribute (NOTE: expects value in milliseconds).
+ # We recommend this parameter to be in sync with session inactivity timeout ("transport.sessions.inactivity_timeout" or TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT) parameter
+ # which is responsible for detection of the stale device connection sessions.
+ # The value of the session inactivity timeout parameter should be greater or equal to the device inactivity timeout.
+ # Note that the session inactivity timeout is set in milliseconds while device inactivity timeout is in seconds.
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}"
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.
@@ -785,6 +790,10 @@ state:
# 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;'
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
+ # Millisecond value defining time-to-live for device state telemetry data (e.g. 'active', 'lastActivityTime').
+ # Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data.
+ # 0 means time-to-live mechanism is disabled.
+ telemetryTtl: "${STATE_TELEMETRY_TTL:0}"
# Tbel parameters
tbel:
@@ -862,10 +871,25 @@ js:
# Transport configuration parameters
transport:
sessions:
- # Inactivity timeout for device session in transport service. The last activity time of the device session is updated if the device sends any message, including keepalive messages
- inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
+ # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.
+ # The parameter value is in milliseconds.
+ # The last activity time of the device session is updated if the device sends any message, including keepalive messages
+ # If there is no activity, the session will be closed, and all subscriptions will be deleted.
+ # We recommend this parameter to be in sync with device inactivity timeout ("state.defaultInactivityTimeoutInSec" or DEFAULT_INACTIVITY_TIMEOUT) parameter
+ # which is responsible for detection of the device connectivity status in the core service of the platform.
+ # The value of the session inactivity timeout parameter should be greater or equal to the device inactivity timeout.
+ # Note that the session inactivity timeout is set in milliseconds while device inactivity timeout is in seconds.
+ inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:600000}"
# Interval of periodic check for expired sessions and report of the changes to session last activity time
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}"
+ activity:
+ # This property specifies the strategy for reporting activity events within each reporting period.
+ # The accepted values are 'FIRST', 'LAST', 'FIRST_AND_LAST' and 'ALL'.
+ # - 'FIRST': Only the first activity event in each reporting period is reported.
+ # - 'LAST': Only the last activity event in the reporting period is reported.
+ # - 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.
+ # - 'ALL': All activity events in the reporting period are reported.
+ reporting_strategy: "${TB_TRANSPORT_ACTIVITY_REPORTING_STRATEGY:LAST}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}"
@@ -1695,4 +1719,4 @@ management:
web:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
- include: '${METRICS_ENDPOINTS_EXPOSE:info}'
\ No newline at end of file
+ include: '${METRICS_ENDPOINTS_EXPOSE:info}'
diff --git a/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java b/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java
new file mode 100644
index 0000000000..a282452f4e
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/tenant/TenantActorTest.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.tenant;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.TbActorCtx;
+import org.thingsboard.server.actors.TbActorRef;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.queue.ServiceType;
+import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg;
+import org.thingsboard.server.dao.tenant.TenantService;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TenantActorTest {
+
+ TenantActor tenantActor;
+ TbActorCtx ctx;
+ ActorSystemContext systemContext;
+ TenantId tenantId = TenantId.SYS_TENANT_ID;
+ DeviceId deviceId = DeviceId.fromString("78bf9b26-74ef-4af2-9cfb-ad6cf24ad2ec");
+
+ @Before
+ public void setUp() throws Exception {
+ systemContext = mock(ActorSystemContext.class);
+ ctx = mock(TbActorCtx.class);
+ tenantActor = (TenantActor) new TenantActor.ActorCreator(systemContext, tenantId).createActor();
+ when(systemContext.getTenantService()).thenReturn(mock(TenantService.class));
+ tenantActor.init(ctx);
+ tenantActor.cantFindTenant = false;
+ }
+
+ @Test
+ public void deleteDeviceTest() {
+ TbActorRef deviceActorRef = mock(TbActorRef.class);
+ when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 0,true));
+ when(ctx.getOrCreateChildActor(any(), any(), any(), any())).thenReturn(deviceActorRef);
+ ComponentLifecycleMsg componentLifecycleMsg = new ComponentLifecycleMsg(tenantId, deviceId, ComponentLifecycleEvent.DELETED);
+ tenantActor.doProcess(componentLifecycleMsg);
+ verify(deviceActorRef).tellWithHighPriority(eq(new DeviceDeleteMsg(tenantId, deviceId)));
+
+ reset(ctx, deviceActorRef);
+ when(systemContext.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(new TopicPartitionInfo("Main", tenantId, 1,false));
+ tenantActor.doProcess(componentLifecycleMsg);
+ verify(ctx, never()).getOrCreateChildActor(any(), any(), any(), any());
+ verify(deviceActorRef, never()).tellWithHighPriority(any());
+ }
+
+}
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
index 429d0b765f..44302f08ba 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
@@ -20,6 +20,7 @@ import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
@@ -47,7 +48,6 @@ import java.util.stream.Collectors;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
-import static org.thingsboard.server.service.entitiy.DefaultTbNotificationEntityService.edgeTypeByActionType;
@Slf4j
public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
@@ -91,7 +91,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
int cntTime = 1;
Mockito.verify(tbClusterService, times(cntTime)).sendNotificationMsgToEdge(Mockito.eq(tenantId),
Mockito.isNull(), Mockito.isNull(), Mockito.any(), Mockito.eq(EdgeEventType.RELATION),
- Mockito.eq(edgeTypeByActionType(actionType)), Mockito.any());
+ Mockito.eq(EdgeUtils.getEdgeEventActionTypeByActionType(actionType)), Mockito.any());
ArgumentMatcher matcherOriginatorId = argument -> argument.equals(relation.getTo());
ArgumentMatcher matcherEntityClassEquals = Objects::isNull;
ArgumentMatcher matcherCustomerId = customerId == null ?
@@ -111,7 +111,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
ActionType actionType, int cntTime) {
Mockito.verify(tbClusterService, times(cntTime)).sendNotificationMsgToEdge(Mockito.eq(tenantId),
Mockito.isNull(), Mockito.isNull(), Mockito.any(), Mockito.eq(EdgeEventType.RELATION),
- Mockito.eq(edgeTypeByActionType(actionType)), Mockito.any());
+ Mockito.eq(EdgeUtils.getEdgeEventActionTypeByActionType(actionType)), Mockito.any());
ArgumentMatcher matcherOriginatorId = argument -> argument.getClass().equals(relation.getFrom().getClass());
ArgumentMatcher matcherEntityClassEquals = Objects::isNull;
ArgumentMatcher matcherCustomerId = customerId == null ?
@@ -317,7 +317,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
private void testNotificationMsgToEdgeServiceNeverWithActionType(EntityId entityId, ActionType actionType) {
EdgeEventActionType edgeEventActionType = ActionType.CREDENTIALS_UPDATED.equals(actionType) ?
- EdgeEventActionType.CREDENTIALS_UPDATED : edgeTypeByActionType(actionType);
+ EdgeEventActionType.CREDENTIALS_UPDATED : EdgeUtils.getEdgeEventActionTypeByActionType(actionType);
Mockito.verify(tbClusterService, never()).sendNotificationMsgToEdge(Mockito.any(), Mockito.any(),
Mockito.any(entityId.getClass()), Mockito.any(), Mockito.any(), Mockito.eq(edgeEventActionType), Mockito.any());
}
@@ -353,7 +353,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
private void testNotificationMsgToEdgeServiceTime(EntityId entityId, TenantId tenantId, ActionType actionType, int cntTime) {
EdgeEventActionType edgeEventActionType = ActionType.CREDENTIALS_UPDATED.equals(actionType) ?
- EdgeEventActionType.CREDENTIALS_UPDATED : edgeTypeByActionType(actionType);
+ EdgeEventActionType.CREDENTIALS_UPDATED : EdgeUtils.getEdgeEventActionTypeByActionType(actionType);
ArgumentMatcher matcherEntityId = cntTime == 1 ? argument -> argument.equals(entityId) :
argument -> argument.getClass().equals(entityId.getClass());
Mockito.verify(tbClusterService, times(cntTime)).sendNotificationMsgToEdge(Mockito.eq(tenantId),
@@ -364,7 +364,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
private void testSendNotificationMsgToEdgeServiceTimeEntityEqAny(TenantId tenantId, ActionType actionType, int cntTime) {
Mockito.verify(tbClusterService, times(cntTime)).sendNotificationMsgToEdge(Mockito.eq(tenantId),
Mockito.any(), Mockito.any(EntityId.class), Mockito.any(), Mockito.isNull(),
- Mockito.eq(edgeTypeByActionType(actionType)), Mockito.any());
+ Mockito.eq(EdgeUtils.getEdgeEventActionTypeByActionType(actionType)), Mockito.any());
}
protected void testBroadcastEntityStateChangeEventTime(EntityId entityId, TenantId tenantId, int cntTime) {
diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
index f5c1fbdae9..1415e38d7e 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
@@ -110,7 +110,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
- testNotifyEntityAllOneTime(alarm, alarm.getId(), alarm.getOriginator(),
+ testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED);
}
@@ -122,7 +122,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
- testNotifyEntityAllOneTime(alarm, alarm.getId(), alarm.getOriginator(),
+ testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED);
}
@@ -772,7 +772,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull("Saved alarm is null!", alarm);
- testNotifyEntityNeverMsgToEdgeServiceOneTime(alarm, alarm.getId(), tenantId, ActionType.ADDED);
+ testNotifyEntityNever(alarm.getId(), alarm);
resetTokens();
diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
index 3761e0c9c1..e272a40857 100644
--- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
@@ -130,13 +130,14 @@ public class ImageControllerTest extends AbstractControllerTest {
checkPngImageDescriptor(imageInfo.getDescriptor(ImageDescriptor.class));
String newFilename = "my_jpeg_image.png";
- imageInfo = uploadImage(HttpMethod.PUT, "/api/images/tenant/" + filename, newFilename, "image/jpeg", JPEG_IMAGE);
+ TbResourceInfo newImageInfo = uploadImage(HttpMethod.PUT, "/api/images/tenant/" + filename, newFilename, "image/jpeg", JPEG_IMAGE);
- assertThat(imageInfo.getTitle()).isEqualTo(filename);
- assertThat(imageInfo.getResourceKey()).isEqualTo(filename);
- assertThat(imageInfo.getFileName()).isEqualTo(newFilename);
+ assertThat(newImageInfo.getTitle()).isEqualTo(filename);
+ assertThat(newImageInfo.getResourceKey()).isEqualTo(filename);
+ assertThat(newImageInfo.getFileName()).isEqualTo(newFilename);
+ assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(imageInfo.getPublicResourceKey());
- ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
+ ImageDescriptor imageDescriptor = newImageInfo.getDescriptor(ImageDescriptor.class);
checkJpegImageDescriptor(imageDescriptor);
assertThat(downloadImage("tenant", filename)).containsExactly(JPEG_IMAGE);
@@ -154,12 +155,15 @@ public class ImageControllerTest extends AbstractControllerTest {
assertThat(imageInfo.getFileName()).isEqualTo(filename);
String newTitle = "My PNG image";
- imageInfo.setTitle(newTitle);
- imageInfo.setDescriptor(JacksonUtil.newObjectNode());
- imageInfo = doPut("/api/images/tenant/" + filename + "/info", imageInfo, TbResourceInfo.class);
-
- assertThat(imageInfo.getTitle()).isEqualTo(newTitle);
- assertThat(imageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor);
+ TbResourceInfo newImageInfo = new TbResourceInfo(imageInfo);
+ newImageInfo.setTitle(newTitle);
+ newImageInfo.setDescriptor(JacksonUtil.newObjectNode());
+ newImageInfo = doPut("/api/images/tenant/" + filename + "/info", newImageInfo, TbResourceInfo.class);
+
+ assertThat(newImageInfo.getTitle()).isEqualTo(newTitle);
+ assertThat(newImageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor);
+ assertThat(newImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey());
+ assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(newImageInfo.getPublicResourceKey());
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
index 8061cf0eab..0a110ef2d0 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
@@ -15,12 +15,17 @@
*/
package org.thingsboard.server.controller;
+import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@@ -28,6 +33,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES;
@@ -51,6 +57,61 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
}
+ @Test
+ public void testTelemetryRequests() throws Exception {
+ loginTenantAdmin();
+ Device device = createDevice();
+
+ var startTs = 1704899727000L; // Wednesday, January 10 15:15:27 GMT
+ var endOfWeek1Ts = 1705269600000L; // Monday, January 15, 2024 0:00:00 GMT+02:00
+ var endOfWeek2Ts = 1705874400000L; // Monday, January 22, 2024 0:00:00 GMT+02:00
+ var endTs = endOfWeek2Ts + TimeUnit.DAYS.toMillis(1) + TimeUnit.HOURS.toMillis(1); // Monday, January 23, 2024 1:00:00 GMT+02:00
+
+ var firstIntervalTs = startTs + (endOfWeek1Ts - startTs) / 2;
+ var secondIntervalTs = endOfWeek1Ts + (endOfWeek2Ts - endOfWeek1Ts) / 2;
+ var thirdIntervalTs = endOfWeek2Ts + (endTs - endOfWeek2Ts) / 2;
+
+ var middleOfTheInterval = startTs + (endTs - startTs) / 2;
+
+ tsService.save(tenantId, device.getId(), new BasicTsKvEntry(1704899728000L, new LongDataEntry("t", 1L))); // Wednesday, January 10 15:15:28 GMT
+ tsService.save(tenantId, device.getId(), new BasicTsKvEntry(1704899729000L, new LongDataEntry("t", 3L))); // Wednesday, January 10 15:15:29 GMT
+ tsService.save(tenantId, device.getId(), new BasicTsKvEntry(endOfWeek1Ts, new LongDataEntry("t", 2L))); // Monday, January 15, 2024 0:00:00 GMT+02:00
+ tsService.save(tenantId, device.getId(), new BasicTsKvEntry(endOfWeek1Ts + 1000, new LongDataEntry("t", 5L))); // Monday, January 15, 2024 0:00:01 GMT+02:00
+ tsService.save(tenantId, device.getId(), new BasicTsKvEntry(endOfWeek2Ts, new LongDataEntry("t", 9L))); // Monday, January 22, 2024 0:00:00 GMT+02:00
+ tsService.save(tenantId, device.getId(), new BasicTsKvEntry(endOfWeek2Ts + 1000, new LongDataEntry("t", 2L))); // Monday, January 22, 2024 0:00:01 GMT+02:00
+
+ ObjectNode result = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
+ "/values/timeseries?keys=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}",
+ ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv");
+ Assert.assertNotNull(result);
+ Assert.assertNotNull(result.get("t"));
+ Assert.assertEquals(3, result.get("t").size());
+
+ var firstIntervalResult = result.get("t").get(0);
+ Assert.assertEquals(4L, firstIntervalResult.get("value").asLong());
+ Assert.assertEquals(firstIntervalTs, firstIntervalResult.get("ts").asLong());
+
+ var secondIntervalResult = result.get("t").get(1);
+ Assert.assertEquals(7L, secondIntervalResult.get("value").asLong());
+ Assert.assertEquals(secondIntervalTs, secondIntervalResult.get("ts").asLong());
+
+ var thirdIntervalResult = result.get("t").get(2);
+ Assert.assertEquals(11L, thirdIntervalResult.get("value").asLong());
+ Assert.assertEquals(thirdIntervalTs, thirdIntervalResult.get("ts").asLong());
+
+ result = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() +
+ "/values/timeseries?keys=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}",
+ ObjectNode.class, startTs, endTs, "SUM", "MONTH", "Europe/Kyiv");
+
+ Assert.assertNotNull(result);
+ Assert.assertNotNull(result.get("t"));
+ Assert.assertEquals(1, result.get("t").size());
+
+ var monthResult = result.get("t").get(0);
+ Assert.assertEquals(22L, monthResult.get("value").asLong());
+ Assert.assertEquals(middleOfTheInterval, monthResult.get("ts").asLong());
+ }
+
@Test
public void testDeleteAllTelemetryWithLatest() throws Exception {
loginTenantAdmin();
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
index 07fa41f8bd..cd47988658 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
@@ -64,6 +65,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.queue.discovery.QueueKey;
import java.util.ArrayList;
import java.util.Collections;
@@ -71,7 +73,6 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@@ -80,6 +81,7 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.argThat;
@@ -700,6 +702,45 @@ public class TenantControllerTest extends AbstractControllerTest {
});
}
+ @Test
+ public void whenTenantIsDeleted_thenDeleteQueues() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = new TenantProfile();
+ tenantProfile.setName("Test profile");
+ TenantProfileData tenantProfileData = new TenantProfileData();
+ tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration());
+ tenantProfile.setProfileData(tenantProfileData);
+ tenantProfile.setIsolatedTbRuleEngine(true);
+ addQueueConfig(tenantProfile, MAIN_QUEUE_NAME);
+ tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ createDifferentTenant();
+ loginSysAdmin();
+ savedDifferentTenant.setTenantProfileId(tenantProfile.getId());
+ savedDifferentTenant = doPost("/api/tenant", savedDifferentTenant, Tenant.class);
+ TenantId tenantId = differentTenantId;
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull();
+ });
+ TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId);
+ assertThat(tpi.getTenantId()).hasValue(tenantId);
+ TbMsg tbMsg = publishTbMsg(tenantId, tpi);
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+ verify(actorContext).tell(argThat(msg -> {
+ return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId());
+ }));
+ });
+
+ deleteDifferentTenant();
+
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNull();
+ assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId))
+ .isInstanceOf(TenantNotFoundException.class);
+
+ verify(queueAdmin).deleteTopic(eq(tpi.getFullTopicName()));
+ });
+ }
+
private TbMsg publishTbMsg(TenantId tenantId, TopicPartitionInfo tpi) {
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, tenantId, TbMsgMetaData.EMPTY, "{\"test\":1}");
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder()
@@ -759,7 +800,7 @@ public class TenantControllerTest extends AbstractControllerTest {
queueConfiguration.setName(queueName);
queueConfiguration.setTopic(topic);
queueConfiguration.setPollInterval(25);
- queueConfiguration.setPartitions(1 + new Random().nextInt(99));
+ queueConfiguration.setPartitions(12);
queueConfiguration.setConsumerPerPartition(true);
queueConfiguration.setPackProcessingTimeout(2000);
SubmitStrategy submitStrategy = new SubmitStrategy();
@@ -799,20 +840,20 @@ public class TenantControllerTest extends AbstractControllerTest {
ArgumentMatcher matcherTenant = cntTime == 1 ? argument -> argument.equals(tenant) :
argument -> argument.getClass().equals(Tenant.class);
if (ComponentLifecycleEvent.DELETED.equals(event)) {
- Mockito.verify(tbClusterService, times( cntTime)).onTenantDelete(Mockito.argThat(matcherTenant),
+ Mockito.verify(tbClusterService, times(cntTime)).onTenantDelete(Mockito.argThat(matcherTenant),
Mockito.isNull());
} else {
- Mockito.verify(tbClusterService, times( cntTime)).onTenantChange(Mockito.argThat(matcherTenant),
+ Mockito.verify(tbClusterService, times(cntTime)).onTenantChange(Mockito.argThat(matcherTenant),
Mockito.isNull());
}
TenantId tenantId = cntTime == 1 ? tenant.getId() : (TenantId) createEntityId_NULL_UUID(tenant);
- testBroadcastEntityStateChangeEventTime(tenantId, tenantId, cntTime);
+ testBroadcastEntityStateChangeEventTime(tenantId, tenantId, cntTime);
Mockito.reset(tbClusterService);
}
private void testBroadcastEntityStateChangeEventNeverTenant() {
Mockito.verify(tbClusterService, never()).onTenantChange(Mockito.any(Tenant.class),
- Mockito.isNull());
+ Mockito.isNull());
testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant()));
Mockito.reset(tbClusterService);
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java
index 5ada0e69d5..239cb87ab4 100644
--- a/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java
@@ -15,21 +15,28 @@
*/
package org.thingsboard.server.edge;
+import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.node.TextNode;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
+import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@@ -84,26 +91,18 @@ public class AlarmEdgeTest extends AbstractEdgeTest {
alarm.setOriginator(device.getId());
alarm.setType("alarm");
alarm.setSeverity(AlarmSeverity.CRITICAL);
- edgeImitator.expectMessageAmount(1);
Alarm savedAlarm = doPost("/api/alarm", alarm, Alarm.class);
- Assert.assertTrue(edgeImitator.waitForMessages());
- AbstractMessage latestMessage = edgeImitator.getLatestMessage();
- Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg);
- AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage;
- Alarm alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true);
- Assert.assertNotNull(alarmMsg);
- Assert.assertEquals(savedAlarm, alarmMsg);
- Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, alarmUpdateMsg.getMsgType());
+ edgeImitator.ignoreType(AlarmCommentUpdateMsg.class);
// ack alarm
edgeImitator.expectMessageAmount(1);
doPost("/api/alarm/" + savedAlarm.getUuidId() + "/ack");
Assert.assertTrue(edgeImitator.waitForMessages());
- latestMessage = edgeImitator.getLatestMessage();
+ AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg);
- alarmUpdateMsg = (AlarmUpdateMsg) latestMessage;
+ AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ALARM_ACK_RPC_MESSAGE, alarmUpdateMsg.getMsgType());
- alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true);
+ Alarm alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true);
Assert.assertNotNull(alarmMsg);
Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType());
Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName());
@@ -137,6 +136,99 @@ public class AlarmEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType());
Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName());
Assert.assertEquals(AlarmStatus.CLEARED_ACK, alarmMsg.getStatus());
+ edgeImitator.allowIgnoredTypes();
+ }
+
+ @Test
+ public void testSendAlarmCommentToCloud() throws Exception {
+ Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge();
+
+ Alarm alarm = buildAlarmForUplinkMsg(device.getId());
+
+ UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
+ AlarmUpdateMsg.Builder alarmUpdateMgBuilder = AlarmUpdateMsg.newBuilder();
+ alarmUpdateMgBuilder.setIdMSB(alarm.getUuidId().getMostSignificantBits());
+ alarmUpdateMgBuilder.setIdLSB(alarm.getUuidId().getLeastSignificantBits());
+ alarmUpdateMgBuilder.setEntity(JacksonUtil.toString(alarm));
+ testAutoGeneratedCodeByProtobuf(alarmUpdateMgBuilder);
+ uplinkMsgBuilder.addAlarmUpdateMsg(alarmUpdateMgBuilder.build());
+
+ testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+ Assert.assertTrue(edgeImitator.waitForResponses());
+
+ AlarmComment alarmComment = buildAlarmCommentForUplinkMsg(alarm.getId());
+
+ uplinkMsgBuilder = UplinkMsg.newBuilder();
+ AlarmCommentUpdateMsg.Builder alarmCommentUpdateMgBuilder = AlarmCommentUpdateMsg.newBuilder();
+ alarmCommentUpdateMgBuilder.setEntity(JacksonUtil.toString(alarmComment));
+ alarmCommentUpdateMgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
+ testAutoGeneratedCodeByProtobuf(alarmCommentUpdateMgBuilder);
+ uplinkMsgBuilder.addAlarmCommentUpdateMsg(alarmCommentUpdateMgBuilder.build());
+
+ testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+ Assert.assertTrue(edgeImitator.waitForResponses());
+
+ PageData pageData = doGetTyped("/api/alarm/" + alarmComment.getAlarmId().getId() + "/comment" + "?page=0&pageSize=1", new TypeReference<>() {});
+ Assert.assertNotNull("Found pageData is null", pageData);
+ Assert.assertNotEquals("Expected alarms are not found!", 0, pageData.getTotalElements());
+
+ Assert.assertNotNull(pageData.getData().get(0));
+ AlarmCommentInfo alarmInfo = pageData.getData().get(0);
+ Assert.assertEquals(alarm.getId(), alarmInfo.getAlarmId());
+ Assert.assertEquals(alarmComment.getAlarmId(), alarmInfo.getAlarmId());
+ }
+
+ @Test
+ public void testAlarmComments() throws Exception {
+ Device device = findDeviceByName("Edge Device 1");
+ Alarm alarm = new Alarm();
+ alarm.setOriginator(device.getId());
+ alarm.setType("alarm");
+ alarm.setSeverity(AlarmSeverity.MINOR);
+ Alarm savedAlarm = doPost("/api/alarm", alarm, Alarm.class);
+
+ // create alarm comment
+ edgeImitator.expectMessageAmount(1);
+ AlarmComment alarmComment = new AlarmComment();
+ alarmComment.setComment(new TextNode("Test"));
+ alarmComment.setAlarmId(savedAlarm.getId());
+ alarmComment = doPost("/api/alarm/" + savedAlarm.getUuidId() + "/comment", alarmComment, AlarmComment.class);
+ Assert.assertTrue(edgeImitator.waitForMessages());
+ AbstractMessage latestMessage = edgeImitator.getLatestMessage();
+ AlarmCommentUpdateMsg alarmCommentUpdateMsg = (AlarmCommentUpdateMsg) latestMessage;
+ Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, alarmCommentUpdateMsg.getMsgType());
+ AlarmComment alarmCommentMsg = JacksonUtil.fromString(alarmCommentUpdateMsg.getEntity(), AlarmComment.class, true);
+ Assert.assertNotNull(alarmCommentMsg);
+ Assert.assertEquals(alarmComment, alarmCommentMsg);
+
+ // update alarm comment
+ edgeImitator.expectMessageAmount(1);
+ alarmComment.setComment(JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")));
+ alarmComment = doPost("/api/alarm/" + savedAlarm.getUuidId() + "/comment", alarmComment, AlarmComment.class);
+ Assert.assertTrue(edgeImitator.waitForMessages());
+ latestMessage = edgeImitator.getLatestMessage();
+ alarmCommentUpdateMsg = (AlarmCommentUpdateMsg) latestMessage;
+ Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, alarmCommentUpdateMsg.getMsgType());
+ alarmCommentMsg = JacksonUtil.fromString(alarmCommentUpdateMsg.getEntity(), AlarmComment.class, true);
+ Assert.assertNotNull(alarmCommentMsg);
+ Assert.assertEquals(alarmComment, alarmCommentMsg);
+
+ // delete alarm
+ edgeImitator.expectMessageAmount(1);
+ doDelete("/api/alarm/" + savedAlarm.getUuidId() + "/comment/" + alarmComment.getUuidId())
+ .andExpect(status().isOk());
+ Assert.assertTrue(edgeImitator.waitForMessages());
+ latestMessage = edgeImitator.getLatestMessage();
+ alarmCommentUpdateMsg = (AlarmCommentUpdateMsg) latestMessage;
+ Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, alarmCommentUpdateMsg.getMsgType());
+ alarmCommentMsg = JacksonUtil.fromString(alarmCommentUpdateMsg.getEntity(), AlarmComment.class, true);
+ Assert.assertNotNull(alarmCommentMsg);
}
private Alarm buildAlarmForUplinkMsg(DeviceId deviceId) {
@@ -148,4 +240,16 @@ public class AlarmEdgeTest extends AbstractEdgeTest {
alarm.setSeverity(AlarmSeverity.CRITICAL);
return alarm;
}
+
+ private AlarmComment buildAlarmCommentForUplinkMsg(AlarmId alarmId) {
+ UUID uuid = Uuids.timeBased();
+ AlarmComment alarmComment = new AlarmComment();
+ alarmComment.setAlarmId(alarmId);
+ alarmComment.setType(AlarmCommentType.OTHER);
+ alarmComment.setUserId(tenantAdminUserId);
+ alarmComment.setId(new AlarmCommentId(uuid));
+ alarmComment.setComment(new TextNode("AlarmComment"));
+ alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid));
+ return alarmComment;
+ }
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
index b79f37f94a..488bd4ed70 100644
--- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
@@ -25,8 +25,10 @@ import io.netty.handler.codec.mqtt.MqttQoS;
import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@@ -44,15 +46,17 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.session.FeatureType;
-import org.thingsboard.server.common.adaptor.JsonConverter;
+import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
@@ -75,6 +79,7 @@ import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -86,6 +91,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_DEVICE_TYPE = "default";
+ @Autowired
+ protected EdgeService edgeService;
+
@Test
public void testDevices() throws Exception {
// create device and assign to edge; update device
@@ -769,6 +777,54 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
}
}
+ @Test
+ public void testVerifyProcessCorrectEdgeUpdateToDeviceActorOnUnassignFromDifferentEdge() throws Exception {
+ Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge();
+
+ // assign device to another edge
+ Edge tmpEdge = doPost("/api/edge", constructEdge("Test Tmp Edge", "test"), Edge.class);
+ doPost("/api/edge/" + tmpEdge.getUuidId()
+ + "/device/" + device.getUuidId(), Device.class);
+ List relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, device.getId());
+ Assert.assertEquals(2, relatedEdgeIds.size());
+
+ // unassign device from edge
+ doDelete("/api/edge/" + edge.getUuidId()
+ + "/device/" + device.getUuidId(), Device.class);
+ relatedEdgeIds = edgeService.findAllRelatedEdgeIds(tenantId, device.getId());
+ Assert.assertEquals(1, relatedEdgeIds.size());
+ Assert.assertEquals(tmpEdge.getId(), relatedEdgeIds.get(0));
+
+ // clean up stored edge events
+ edgeEventService.cleanupEvents(1);
+
+ // perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly
+ doPostAsync(
+ "/api/rpc/oneway/" + device.getId().getId().toString(),
+ JacksonUtil.toString(createDefaultRpc()),
+ String.class,
+ status().isOk());
+
+ final AtomicReference> resultRef = new AtomicReference<>();
+ Awaitility.await()
+ .atMost(TIMEOUT, TimeUnit.SECONDS)
+ .until(() -> {
+ PageData result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1));
+ resultRef.set(result);
+ return result != null && result.getData().size() == 1;
+ });
+
+ PageData result = resultRef.get();
+ EdgeEvent edgeEvent = result.getData().get(0);
+ Assert.assertEquals(EdgeEventActionType.RPC_CALL, edgeEvent.getAction());
+ Assert.assertEquals(EdgeEventType.DEVICE, edgeEvent.getType());
+ Assert.assertEquals(tmpEdge.getId(), edgeEvent.getEdgeId());
+ Assert.assertEquals(device.getId().getId(), edgeEvent.getEntityId());
+
+ // clean up tmp edge
+ doDelete("/api/edge/" + tmpEdge.getId().getId().toString()).andExpect(status().isOk());
+ }
+
private Device buildDeviceForUplinkMsg(String name, String type) {
Device device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
@@ -778,7 +834,6 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
return device;
}
-
private DeviceCredentials buildDeviceCredentialsForUplinkMsg(DeviceId deviceId) {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(deviceId);
@@ -786,4 +841,20 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
return deviceCredentials;
}
+
+ private ObjectNode createDefaultRpc() {
+ ObjectNode rpc = JacksonUtil.newObjectNode();
+ rpc.put("method", "setGpio");
+
+ ObjectNode params = JacksonUtil.newObjectNode();
+
+ params.put("pin", 7);
+ params.put("value", 1);
+
+ rpc.set("params", params);
+ rpc.put("persistent", true);
+ rpc.put("timeout", 5000);
+
+ return rpc;
+ }
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
index f7c464fab9..f5e018ed3a 100644
--- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
+++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
@@ -27,6 +27,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
@@ -231,6 +232,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(alarmUpdateMsg));
}
}
+ if (downlinkMsg.getAlarmCommentUpdateMsgCount() > 0) {
+ for (AlarmCommentUpdateMsg alarmCommentUpdateMsg : downlinkMsg.getAlarmCommentUpdateMsgList()) {
+ result.add(saveDownlinkMsg(alarmCommentUpdateMsg));
+ }
+ }
if (downlinkMsg.getEntityDataCount() > 0) {
for (EntityDataProto entityData : downlinkMsg.getEntityDataList()) {
if (randomFailuresOnTimeseriesDownlink) {
diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
index 384c00c8ed..9c45977a7c 100644
--- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
@@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@@ -157,6 +158,9 @@ public abstract class BaseEdgeProcessorTest {
@MockBean
protected AlarmService alarmService;
+ @MockBean
+ protected AlarmCommentService alarmCommentService;
+
@MockBean
protected DeviceService deviceService;
diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java
index f628037c5d..6efb171fb4 100644
--- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java
@@ -29,11 +29,12 @@ import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.ServiceType;
+import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
-import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
@@ -73,6 +74,8 @@ public class DefaultTbClusterServiceTest {
@MockBean
protected GatewayNotificationsService gatewayNotificationsService;
@MockBean
+ protected EdgeService edgeService;
+ @MockBean
protected PartitionService partitionService;
@MockBean
protected TbQueueProducerProvider producerProvider;
diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
index c03c2babaa..6bd0cd9c0e 100644
--- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
@@ -447,7 +447,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
verifyMsgProcessed(consumer1.testMsg);
verifyMsgProcessed(consumer2.testMsg);
- consumerManager.delete();
+ consumerManager.delete(true);
await().atMost(2, TimeUnit.SECONDS)
.untilAsserted(() -> {
@@ -488,7 +488,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
verifySubscribedAndLaunched(consumer, partitions);
verifyMsgProcessed(consumer.testMsg);
- consumerManager.delete();
+ consumerManager.delete(true);
await().atMost(2, TimeUnit.SECONDS)
.untilAsserted(() -> {
diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml
index 3d0ce3f933..981bcab132 100644
--- a/application/src/test/resources/logback-test.xml
+++ b/application/src/test/resources/logback-test.xml
@@ -38,8 +38,6 @@
-
-
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java
index 7bee9f6983..c822693a71 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java
@@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
public interface AlarmCommentService {
+
AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment);
AlarmComment saveAlarmComment(TenantId tenantId, AlarmComment alarmComment);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
index 4c05438d03..3dd9626739 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
@@ -47,4 +47,6 @@ public interface AttributesService {
List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds);
+ List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds, String scope);
+
}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
index 08dd64275a..2e65a58647 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -38,7 +38,7 @@ public interface AuditLogService {
PageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink);
- ListenableFuture> logEntityAction(
+ ListenableFuture logEntityAction(
TenantId tenantId,
CustomerId customerId,
UserId userId,
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index 54fd39906e..53b5d2ec63 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -136,5 +136,6 @@ public class DataConstants {
public static final String HP_QUEUE_TOPIC = "tb_rule_engine.hp";
public static final String SQ_QUEUE_NAME = "SequentialByOriginator";
public static final String SQ_QUEUE_TOPIC = "tb_rule_engine.sq";
+ public static final String QUEUE_NAME = "queueName";
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java
index dc969bb362..69a3fc7d8c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
@@ -32,6 +33,7 @@ import java.util.concurrent.ThreadLocalRandom;
public final class EdgeUtils {
private static final EnumMap entityTypeEdgeEventTypeEnumMap;
+ private static final EnumMap actionTypeEdgeEventActionTypeEnumMap;
static {
entityTypeEdgeEventTypeEnumMap = new EnumMap<>(EntityType.class);
@@ -40,6 +42,13 @@ public final class EdgeUtils {
entityTypeEdgeEventTypeEnumMap.put(edgeEventType.getEntityType(), edgeEventType);
}
}
+
+ actionTypeEdgeEventActionTypeEnumMap = new EnumMap<>(ActionType.class);
+ for (EdgeEventActionType edgeEventActionType : EdgeEventActionType.values()) {
+ if (edgeEventActionType.getActionType() != null) {
+ actionTypeEdgeEventActionTypeEnumMap.put(edgeEventActionType.getActionType(), edgeEventActionType);
+ }
+ }
}
private static final int STACK_TRACE_LIMIT = 10;
@@ -54,6 +63,10 @@ public final class EdgeUtils {
return entityTypeEdgeEventTypeEnumMap.get(entityType);
}
+ public static EdgeEventActionType getEdgeEventActionTypeByActionType(ActionType actionType) {
+ return actionTypeEdgeEventActionTypeEnumMap.get(actionType);
+ }
+
public static EdgeEvent constructEdgeEvent(TenantId tenantId,
EdgeId edgeId,
EdgeEventType type,
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
index e355901182..680f744c7b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
@@ -15,26 +15,39 @@
*/
package org.thingsboard.server.common.data.edge;
+import lombok.Getter;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+@Getter
public enum EdgeEventActionType {
- ADDED,
- DELETED,
- UPDATED,
- POST_ATTRIBUTES,
- ATTRIBUTES_UPDATED,
- ATTRIBUTES_DELETED,
- TIMESERIES_UPDATED,
- CREDENTIALS_UPDATED,
- ASSIGNED_TO_CUSTOMER,
- UNASSIGNED_FROM_CUSTOMER,
- RELATION_ADD_OR_UPDATE,
- RELATION_DELETED,
- RPC_CALL,
- ALARM_ACK,
- ALARM_CLEAR,
- ALARM_ASSIGNED,
- ALARM_UNASSIGNED,
- ASSIGNED_TO_EDGE,
- UNASSIGNED_FROM_EDGE,
- CREDENTIALS_REQUEST,
- ENTITY_MERGE_REQUEST // deprecated
-}
\ No newline at end of file
+ ADDED(ActionType.ADDED),
+ UPDATED(ActionType.UPDATED),
+ DELETED(ActionType.DELETED),
+ POST_ATTRIBUTES(null),
+ ATTRIBUTES_UPDATED(ActionType.ATTRIBUTES_UPDATED),
+ ATTRIBUTES_DELETED(ActionType.ATTRIBUTES_DELETED),
+ TIMESERIES_UPDATED(ActionType.TIMESERIES_UPDATED),
+ CREDENTIALS_UPDATED(ActionType.CREDENTIALS_UPDATED),
+ ASSIGNED_TO_CUSTOMER(ActionType.ASSIGNED_TO_CUSTOMER),
+ UNASSIGNED_FROM_CUSTOMER(ActionType.UNASSIGNED_FROM_CUSTOMER),
+ RELATION_ADD_OR_UPDATE(ActionType.RELATION_ADD_OR_UPDATE),
+ RELATION_DELETED(ActionType.RELATION_DELETED),
+ RPC_CALL(ActionType.RPC_CALL),
+ ALARM_ACK(ActionType.ALARM_ACK),
+ ALARM_CLEAR(ActionType.ALARM_CLEAR),
+ ALARM_ASSIGNED(ActionType.ALARM_ASSIGNED),
+ ALARM_UNASSIGNED(ActionType.ALARM_UNASSIGNED),
+ ADDED_COMMENT(ActionType.ADDED_COMMENT),
+ UPDATED_COMMENT(ActionType.UPDATED_COMMENT),
+ DELETED_COMMENT(ActionType.DELETED_COMMENT),
+ ASSIGNED_TO_EDGE(ActionType.ASSIGNED_TO_EDGE),
+ UNASSIGNED_FROM_EDGE(ActionType.UNASSIGNED_FROM_EDGE),
+ CREDENTIALS_REQUEST(null),
+ ENTITY_MERGE_REQUEST(null); // deprecated
+
+ private final ActionType actionType;
+
+ EdgeEventActionType(ActionType actionType) {
+ this.actionType = actionType;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
index a8627d3c0d..6fc255501f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
@@ -27,6 +27,7 @@ public enum EdgeEventType {
ASSET_PROFILE(true, EntityType.ASSET_PROFILE),
ENTITY_VIEW(false, EntityType.ENTITY_VIEW),
ALARM(false, EntityType.ALARM),
+ ALARM_COMMENT(false, null),
RULE_CHAIN(false, EntityType.RULE_CHAIN),
RULE_CHAIN_METADATA(false, null),
EDGE(false, EntityType.EDGE),
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AggregationParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AggregationParams.java
new file mode 100644
index 0000000000..6c85cfb3c0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AggregationParams.java
@@ -0,0 +1,92 @@
+/**
+ * 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.kv;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.StringUtils;
+
+import java.time.DateTimeException;
+import java.time.ZoneId;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@AllArgsConstructor
+@EqualsAndHashCode
+@Slf4j
+public class AggregationParams {
+ private static final Map TZ_LINKS = Map.of("EST", "America/New_York", "GMT+0", "GMT", "GMT-0", "GMT", "HST", "US/Hawaii", "MST", "America/Phoenix", "ROC", "Asia/Taipei");
+ @Getter
+ private final Aggregation aggregation;
+ @Getter
+ private final IntervalType intervalType;
+ @Getter
+ private final ZoneId tzId;
+
+ private final long interval;
+
+ public static AggregationParams none() {
+ return new AggregationParams(Aggregation.NONE, null, null, 0L);
+ }
+
+ public static AggregationParams milliseconds(Aggregation aggregationType, long aggregationIntervalMs) {
+ return new AggregationParams(aggregationType, IntervalType.MILLISECONDS, null, aggregationIntervalMs);
+ }
+
+ public static AggregationParams calendar(Aggregation aggregationType, IntervalType intervalType, String tzIdStr) {
+ return calendar(aggregationType, intervalType, getZoneId(tzIdStr));
+ }
+
+ public static AggregationParams calendar(Aggregation aggregationType, IntervalType intervalType, ZoneId tzId) {
+ return new AggregationParams(aggregationType, intervalType, tzId, 0L);
+ }
+
+ public static AggregationParams of(Aggregation aggregation, IntervalType intervalType, ZoneId tzId, long interval) {
+ return new AggregationParams(aggregation, intervalType, tzId, interval);
+ }
+
+ public long getInterval() {
+ if (intervalType == null) {
+ return 0L;
+ } else {
+ switch (intervalType) {
+ case WEEK:
+ case WEEK_ISO:
+ return TimeUnit.DAYS.toMillis(7);
+ case MONTH:
+ return TimeUnit.DAYS.toMillis(30);
+ case QUARTER:
+ return TimeUnit.DAYS.toMillis(90);
+ default:
+ return interval;
+ }
+ }
+ }
+
+ private static ZoneId getZoneId(String tzIdStr) {
+ if (StringUtils.isEmpty(tzIdStr)) {
+ return ZoneId.systemDefault();
+ }
+ try {
+ return ZoneId.of(tzIdStr, TZ_LINKS);
+ } catch (DateTimeException e) {
+ log.warn("[{}] Failed to convert the time zone. Fallback to default.", tzIdStr);
+ return ZoneId.systemDefault();
+ }
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
index 54b3a15bd2..09d52e46a1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
@@ -18,41 +18,47 @@ package org.thingsboard.server.common.data.kv;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import java.time.ZoneId;
+
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseReadTsKvQuery extends BaseTsKvQuery implements ReadTsKvQuery {
- private final long interval;
+ private final AggregationParams aggParameters;
private final int limit;
- private final Aggregation aggregation;
private final String order;
public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation) {
this(key, startTs, endTs, interval, limit, aggregation, "DESC");
}
- public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, String order) {
+ public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, String descOrder) {
+ this(key, startTs, endTs, AggregationParams.of(aggregation, IntervalType.MILLISECONDS, ZoneId.systemDefault(), interval), limit, descOrder);
+ }
+
+ public BaseReadTsKvQuery(String key, long startTs, long endTs, AggregationParams parameters, int limit) {
+ this(key, startTs, endTs, parameters, limit, "DESC");
+ }
+
+ public BaseReadTsKvQuery(String key, long startTs, long endTs, AggregationParams parameters, int limit, String order) {
super(key, startTs, endTs);
- this.interval = interval;
+ this.aggParameters = parameters;
this.limit = limit;
- this.aggregation = aggregation;
this.order = order;
}
public BaseReadTsKvQuery(String key, long startTs, long endTs) {
- this(key, startTs, endTs, endTs - startTs, 1, Aggregation.AVG, "DESC");
+ this(key, startTs, endTs, AggregationParams.milliseconds(Aggregation.AVG, endTs - startTs), 1, "DESC");
}
public BaseReadTsKvQuery(String key, long startTs, long endTs, int limit, String order) {
- this(key, startTs, endTs, endTs - startTs, limit, Aggregation.NONE, order);
+ this(key, startTs, endTs, AggregationParams.none(), limit, order);
}
public BaseReadTsKvQuery(ReadTsKvQuery query, long startTs, long endTs) {
super(query.getId(), query.getKey(), startTs, endTs);
- this.interval = query.getInterval();
+ this.aggParameters = query.getAggParameters();
this.limit = query.getLimit();
- this.aggregation = query.getAggregation();
this.order = query.getOrder();
}
-
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/IntervalType.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/IntervalType.java
new file mode 100644
index 0000000000..121b5993e0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/IntervalType.java
@@ -0,0 +1,22 @@
+/**
+ * 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.kv;
+
+public enum IntervalType {
+
+ MILLISECONDS, WEEK/*Sunday-Saturday*/, WEEK_ISO/*Monday-Sunday*/, MONTH, QUARTER
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java
index e979ec665a..776179967f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQuery.java
@@ -17,11 +17,17 @@ package org.thingsboard.server.common.data.kv;
public interface ReadTsKvQuery extends TsKvQuery {
- long getInterval();
+ AggregationParams getAggParameters();
- int getLimit();
+ default long getInterval(){
+ return getAggParameters().getInterval();
+ }
+
+ default Aggregation getAggregation() {
+ return getAggParameters().getAggregation();
+ }
- Aggregation getAggregation();
+ int getLimit();
String getOrder();
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java
index 19c3fc8f4f..8d46a46781 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.page;
+import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
@@ -64,8 +65,8 @@ public abstract class BasePageDataIterable implements Iterable, Iterator pageData = fetchPageData(link);
currentIdx = 0;
- currentItems = pageData.getData();
- hasNextPack = pageData.hasNext();
+ currentItems = pageData != null ? pageData.getData() : new ArrayList<>();
+ hasNextPack = pageData != null && pageData.hasNext();
nextPackLink = link.nextPageLink();
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
index 639aba31e2..ee117a6a86 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
@@ -55,6 +55,8 @@ public class ComponentDescriptor extends BaseData {
@Length(fieldName = "actions")
@ApiModelProperty(position = 10, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private String actions;
+ @ApiModelProperty(position = 11, value = "Indicates that the RuleNode supports queue name configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "true")
+ @Getter @Setter private boolean hasQueueName;
public ComponentDescriptor() {
super();
@@ -74,6 +76,7 @@ public class ComponentDescriptor extends BaseData {
this.configurationDescriptor = plugin.getConfigurationDescriptor();
this.configurationVersion = plugin.getConfigurationVersion();
this.actions = plugin.getActions();
+ this.hasQueueName = plugin.isHasQueueName();
}
@ApiModelProperty(position = 1, value = "JSON object with the descriptor Id. " +
@@ -104,6 +107,8 @@ public class ComponentDescriptor extends BaseData {
if (!Objects.equals(actions, that.actions)) return false;
if (!Objects.equals(configurationDescriptor, that.configurationDescriptor)) return false;
if (configurationVersion != that.configurationVersion) return false;
+ if (clusteringMode != that.clusteringMode) return false;
+ if (hasQueueName != that.isHasQueueName()) return false;
return Objects.equals(clazz, that.clazz);
}
@@ -115,6 +120,8 @@ public class ComponentDescriptor extends BaseData {
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (clazz != null ? clazz.hashCode() : 0);
result = 31 * result + (actions != null ? actions.hashCode() : 0);
+ result = 31 * result + (clusteringMode != null ? clusteringMode.hashCode() : 0);
+ result = 31 * result + (hasQueueName ? 1 : 0);
return result;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
index 97349951ec..4d1e696d1a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
@@ -50,9 +50,11 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements
private boolean debugMode;
@ApiModelProperty(position = 7, value = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode;
- @ApiModelProperty(position = 8, value = "Version of rule node configuration. ", example = "0")
+ @ApiModelProperty(position = 8, value = "Queue name. ", example = "Main")
+ private String queueName;
+ @ApiModelProperty(position = 9, value = "Version of rule node configuration. ", example = "0")
private int configurationVersion;
- @ApiModelProperty(position = 9, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
+ @ApiModelProperty(position = 10, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
private transient JsonNode configuration;
@JsonIgnore
private byte[] configurationBytes;
diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto
index 2f33c21fd8..34a8c6f093 100644
--- a/common/edge-api/src/main/proto/edge.proto
+++ b/common/edge-api/src/main/proto/edge.proto
@@ -325,6 +325,11 @@ message AlarmUpdateMsg {
string entity = 18;
}
+message AlarmCommentUpdateMsg {
+ UpdateMsgType msgType = 1;
+ string entity = 2;
+}
+
message CustomerUpdateMsg {
UpdateMsgType msgType = 1;
int64 idMSB = 2;
@@ -618,6 +623,7 @@ message UplinkMsg {
repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 19;
repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20;
repeated ResourceUpdateMsg resourceUpdateMsg = 21;
+ repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22;
}
message UplinkResponseMsg {
@@ -661,5 +667,6 @@ message DownlinkMsg {
repeated TenantUpdateMsg tenantUpdateMsg = 26;
repeated TenantProfileUpdateMsg tenantProfileUpdateMsg = 27;
repeated ResourceUpdateMsg resourceUpdateMsg = 28;
+ repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 29;
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
index 1b496c8aca..c05bc77df3 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
@@ -35,7 +35,6 @@ import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import javax.annotation.PostConstruct;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -190,14 +189,25 @@ public class HashPartitionService implements PartitionService {
myPartitions.remove(queueKey);
partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey);
- //TODO: remove after merging tb entity services
- removeTenant(tenantId);
-
+ evictTenantInfo(tenantId);
if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) {
publishPartitionChangeEvent(ServiceType.TB_RULE_ENGINE, Map.of(queueKey, Collections.emptySet()));
}
}
+ @Override
+ public void removeTenant(TenantId tenantId) {
+ List queueKeys = partitionSizesMap.keySet().stream()
+ .filter(queueKey -> tenantId.equals(queueKey.getTenantId()))
+ .collect(Collectors.toList());
+ queueKeys.forEach(queueKey -> {
+ myPartitions.remove(queueKey);
+ partitionTopicsMap.remove(queueKey);
+ partitionSizesMap.remove(queueKey);
+ });
+ evictTenantInfo(tenantId);
+ }
+
@Override
public boolean isManagedByCurrentService(TenantId tenantId) {
Set assignedTenantProfiles = serviceInfoProvider.getAssignedTenantProfiles();
@@ -241,7 +251,12 @@ public class HashPartitionService implements PartitionService {
@Override
public boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId) {
- return resolve(serviceType, tenantId, entityId).isMyPartition();
+ try {
+ return resolve(serviceType, tenantId, entityId).isMyPartition();
+ } catch (TenantNotFoundException e) {
+ log.warn("Tenant with id {} not found", tenantId, new RuntimeException("stacktrace"));
+ return false;
+ }
}
private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
@@ -258,6 +273,7 @@ public class HashPartitionService implements PartitionService {
@Override
public synchronized void recalculatePartitions(ServiceInfo currentService, List otherServices) {
+ log.info("Recalculating partitions");
tbTransportServicesByType.clear();
responsibleServices.clear();
logServiceInfo(currentService);
@@ -274,9 +290,14 @@ public class HashPartitionService implements PartitionService {
final ConcurrentMap> newPartitions = new ConcurrentHashMap<>();
partitionSizesMap.forEach((queueKey, size) -> {
for (int i = 0; i < size; i++) {
- ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i);
- if (currentService.equals(serviceInfo)) {
- newPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i);
+ try {
+ ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), queueKey, i);
+ log.trace("Server responsible for {}[{}] - {}", queueKey, i, serviceInfo != null ? serviceInfo.getServiceId() : "none");
+ if (currentService.equals(serviceInfo)) {
+ newPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i);
+ }
+ } catch (Exception e) {
+ log.warn("Failed to resolve server responsible for {}[{}]", queueKey, i, e);
}
}
});
@@ -360,7 +381,12 @@ public class HashPartitionService implements PartitionService {
.collect(Collectors.toList()))
.collect(Collectors.joining(System.lineSeparator())));
}
- applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, serviceType, partitionsMap));
+ PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap);
+ try {
+ applicationEventPublisher.publishEvent(event);
+ } catch (Exception e) {
+ log.error("Failed to publish partition change event {}", event, e);
+ }
}
@Override
@@ -399,7 +425,7 @@ public class HashPartitionService implements PartitionService {
}
@Override
- public void removeTenant(TenantId tenantId) {
+ public void evictTenantInfo(TenantId tenantId) {
tenantRoutingInfoMap.remove(tenantId);
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
index 48e5b55ece..2b2479f59b 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
@@ -59,7 +59,7 @@ public interface PartitionService {
int resolvePartitionIndex(UUID entityId, int partitions);
- void removeTenant(TenantId tenantId);
+ void evictTenantInfo(TenantId tenantId);
int countTransportsByType(String type);
@@ -67,6 +67,8 @@ public interface PartitionService {
void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg);
+ void removeTenant(TenantId tenantId);
+
boolean isManagedByCurrentService(TenantId tenantId);
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java
index 42039bf417..2d6d707731 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java
@@ -41,7 +41,11 @@ public abstract class TbApplicationEventListener i
seqNumberLock.unlock();
}
if (validUpdate && filterTbApplicationEvent(event)) {
- onTbApplicationEvent(event);
+ try {
+ onTbApplicationEvent(event);
+ } catch (Exception e) {
+ log.error("Failed to handle partition change event: {}", event, e);
+ }
} else {
log.info("Application event ignored due to invalid sequence number ({} > {}). Event: {}", lastProcessedSequenceNumber, event.getSequenceNumber(), event);
}
diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
index 1e68779b70..735ec1d524 100644
--- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
+++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
@@ -212,7 +212,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
public void reportActivity() {
for (TbCoapClientState state : clients.values()) {
if (state.getSession() != null) {
- transportService.reportActivity(state.getSession());
+ transportService.recordActivity(state.getSession());
}
}
}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 90bddd2edc..8360111a54 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -322,7 +322,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
case PINGREQ:
if (checkConnected(ctx, msg)) {
ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
- transportService.reportActivity(deviceSessionCtx.getSessionInfo());
+ transportService.recordActivity(deviceSessionCtx.getSessionInfo());
}
break;
case DISCONNECT:
@@ -350,7 +350,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
if (topicName.startsWith(MqttTopics.BASE_GATEWAY_API_TOPIC)) {
if (gatewaySessionHandler != null) {
handleGatewayPublishMsg(ctx, topicName, msgId, mqttMsg);
- transportService.reportActivity(deviceSessionCtx.getSessionInfo());
+ transportService.recordActivity(deviceSessionCtx.getSessionInfo());
} else {
log.error("[gatewaySessionHandler] is null, [{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId);
}
@@ -526,7 +526,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg));
attrReqTopicType = TopicType.V2;
} else {
- transportService.reportActivity(deviceSessionCtx.getSessionInfo());
+ transportService.recordActivity(deviceSessionCtx.getSessionInfo());
ack(ctx, msgId, ReturnCode.TOPIC_NAME_INVALID);
}
} catch (AdaptorException e) {
@@ -796,7 +796,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
if (!activityReported) {
- transportService.reportActivity(deviceSessionCtx.getSessionInfo());
+ transportService.recordActivity(deviceSessionCtx.getSessionInfo());
}
ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList));
}
@@ -894,7 +894,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
if (!activityReported) {
- transportService.reportActivity(deviceSessionCtx.getSessionInfo());
+ transportService.recordActivity(deviceSessionCtx.getSessionInfo());
}
ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId(), unSubResults));
}
diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
index 5d41918cab..3a8811df97 100644
--- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
+++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
@@ -416,7 +416,7 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
}
private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
- transportService.reportActivity(sessionInfo);
+ transportService.recordActivity(sessionInfo);
}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
index f51b399a5b..d394af3101 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
@@ -145,7 +145,7 @@ public interface TransportService {
SessionMetaData registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout);
- void reportActivity(SessionInfoProto sessionInfo);
+ void recordActivity(SessionInfoProto sessionInfo);
void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error);
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/AbstractActivityManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/AbstractActivityManager.java
new file mode 100644
index 0000000000..f754d6f5b1
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/AbstractActivityManager.java
@@ -0,0 +1,176 @@
+/**
+ * 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.transport.activity;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy;
+import org.thingsboard.server.queue.scheduler.SchedulerComponent;
+
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Slf4j
+public abstract class AbstractActivityManager implements ActivityManager {
+
+ private final ConcurrentMap states = new ConcurrentHashMap<>();
+
+ @Autowired
+ protected SchedulerComponent scheduler;
+
+ @Data
+ private class ActivityStateWrapper {
+
+ private volatile ActivityState state;
+ private volatile long lastReportedTime;
+ private volatile ActivityStrategy strategy;
+
+ }
+
+ protected void init() {
+ var reportingPeriodMillis = getReportingPeriodMillis();
+ scheduler.scheduleAtFixedRate(this::onReportingPeriodEnd, new Random().nextInt((int) reportingPeriodMillis), reportingPeriodMillis, TimeUnit.MILLISECONDS);
+ }
+
+ protected abstract long getReportingPeriodMillis();
+
+ protected abstract ActivityStrategy getStrategy();
+
+ protected abstract ActivityState updateState(Key key, ActivityState state);
+
+ protected abstract boolean hasExpired(long lastRecordedTime);
+
+ protected abstract void onStateExpiry(Key key, Metadata metadata);
+
+ protected abstract void reportActivity(Key key, Metadata metadata, long timeToReport, ActivityReportCallback callback);
+
+ @Override
+ public void onActivity(Key key, Metadata metadata, long newLastRecordedTime) {
+ if (key == null) {
+ log.error("Failed to process activity event: provided activity key is null.");
+ return;
+ }
+ log.debug("Received activity event for key: [{}]", key);
+
+ var shouldReport = new AtomicBoolean(false);
+ var lastRecordedTime = new AtomicLong();
+ var lastReportedTime = new AtomicLong();
+
+ states.compute(key, (__, stateWrapper) -> {
+ if (stateWrapper == null) {
+ ActivityState newState = new ActivityState<>();
+ stateWrapper = new ActivityStateWrapper();
+ stateWrapper.setState(newState);
+ stateWrapper.setStrategy(getStrategy());
+ }
+ var state = stateWrapper.getState();
+ state.setMetadata(metadata);
+ if (state.getLastRecordedTime() < newLastRecordedTime) {
+ state.setLastRecordedTime(newLastRecordedTime);
+ }
+ shouldReport.set(stateWrapper.getStrategy().onActivity());
+ lastRecordedTime.set(state.getLastRecordedTime());
+ lastReportedTime.set(stateWrapper.getLastReportedTime());
+ return stateWrapper;
+ });
+
+ if (shouldReport.get() && lastReportedTime.get() < lastRecordedTime.get()) {
+ log.debug("Going to report first activity event for key: [{}].", key);
+ reportActivity(key, metadata, lastRecordedTime.get(), new ActivityReportCallback<>() {
+ @Override
+ public void onSuccess(Key key, long reportedTime) {
+ updateLastReportedTime(key, reportedTime);
+ }
+
+ @Override
+ public void onFailure(Key key, Throwable t) {
+ log.debug("Failed to report first activity event for key: [{}].", key, t);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onReportingPeriodEnd() {
+ log.debug("Going to end reporting period.");
+ for (Map.Entry entry : states.entrySet()) {
+ var key = entry.getKey();
+ var stateWrapper = entry.getValue();
+ var currentState = stateWrapper.getState();
+
+ long lastRecordedTime = currentState.getLastRecordedTime();
+ long lastReportedTime = stateWrapper.getLastReportedTime();
+ var metadata = currentState.getMetadata();
+
+ boolean hasExpired;
+ boolean shouldReport;
+
+ var updatedState = updateState(key, currentState);
+ if (updatedState != null) {
+ stateWrapper.setState(updatedState);
+ lastRecordedTime = updatedState.getLastRecordedTime();
+ metadata = updatedState.getMetadata();
+ hasExpired = hasExpired(lastRecordedTime);
+ shouldReport = stateWrapper.getStrategy().onReportingPeriodEnd();
+ } else {
+ states.remove(key);
+ hasExpired = false;
+ shouldReport = true;
+ }
+
+ if (hasExpired) {
+ states.remove(key);
+ onStateExpiry(key, metadata);
+ shouldReport = true;
+ }
+
+ if (shouldReport && lastReportedTime < lastRecordedTime) {
+ log.debug("Going to report last activity event for key: [{}].", key);
+ reportActivity(key, metadata, lastRecordedTime, new ActivityReportCallback<>() {
+ @Override
+ public void onSuccess(Key key, long reportedTime) {
+ updateLastReportedTime(key, reportedTime);
+ }
+
+ @Override
+ public void onFailure(Key key, Throwable t) {
+ log.debug("Failed to report last activity event for key: [{}].", key, t);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public long getLastRecordedTime(Key key) {
+ ActivityStateWrapper stateWrapper = states.get(key);
+ return stateWrapper == null ? 0L : stateWrapper.getState().getLastRecordedTime();
+ }
+
+ private void updateLastReportedTime(Key key, long newLastReportedTime) {
+ states.computeIfPresent(key, (__, stateWrapper) -> {
+ stateWrapper.setLastReportedTime(Math.max(stateWrapper.getLastReportedTime(), newLastReportedTime));
+ return stateWrapper;
+ });
+ }
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityManager.java
new file mode 100644
index 0000000000..5d27738429
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityManager.java
@@ -0,0 +1,26 @@
+/**
+ * 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.transport.activity;
+
+public interface ActivityManager {
+
+ void onActivity(Key key, Metadata metadata, long activityTimeMillis);
+
+ void onReportingPeriodEnd();
+
+ long getLastRecordedTime(Key key);
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityReportCallback.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityReportCallback.java
new file mode 100644
index 0000000000..c58f2bfa34
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityReportCallback.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity;
+
+public interface ActivityReportCallback {
+
+ void onSuccess(Key key, long reportedTime);
+
+ void onFailure(Key key, Throwable t);
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityState.java
similarity index 63%
rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java
rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityState.java
index 681cc67cb0..bde5e05645 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/ActivityState.java
@@ -13,18 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.rule.engine.flow;
+package org.thingsboard.server.common.transport.activity;
import lombok.Data;
-import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
-public class TbCheckpointNodeConfiguration implements NodeConfiguration {
+public class ActivityState {
- private String queueName;
+ private volatile long lastRecordedTime;
+ private volatile Metadata metadata;
- @Override
- public TbCheckpointNodeConfiguration defaultConfiguration() {
- return new TbCheckpointNodeConfiguration();
- }
}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategy.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategy.java
new file mode 100644
index 0000000000..b8fbb9ff12
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategy.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity.strategy;
+
+public interface ActivityStrategy {
+
+ boolean onActivity();
+
+ boolean onReportingPeriodEnd();
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategyType.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategyType.java
new file mode 100644
index 0000000000..3f2ec74c3c
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategyType.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity.strategy;
+
+public enum ActivityStrategyType {
+
+ ALL {
+ @Override
+ public ActivityStrategy toStrategy() {
+ return AllEventsActivityStrategy.getInstance();
+ }
+ },
+ FIRST {
+ @Override
+ public ActivityStrategy toStrategy() {
+ return new FirstEventActivityStrategy();
+ }
+ },
+ LAST {
+ @Override
+ public ActivityStrategy toStrategy() {
+ return LastEventActivityStrategy.getInstance();
+ }
+ },
+ FIRST_AND_LAST {
+ @Override
+ public ActivityStrategy toStrategy() {
+ return new FirstAndLastEventActivityStrategy();
+ }
+ };
+
+ public abstract ActivityStrategy toStrategy();
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionActivityData.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/AllEventsActivityStrategy.java
similarity index 51%
rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionActivityData.java
rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/AllEventsActivityStrategy.java
index 3e0be50ce6..f7d2679689 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionActivityData.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/AllEventsActivityStrategy.java
@@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.transport.service;
+package org.thingsboard.server.common.transport.activity.strategy;
-import lombok.Data;
-import org.thingsboard.server.gen.transport.TransportProtos;
+public final class AllEventsActivityStrategy implements ActivityStrategy {
-/**
- * Created by ashvayka on 15.10.18.
- */
-@Data
-public class SessionActivityData {
+ private static final AllEventsActivityStrategy INSTANCE = new AllEventsActivityStrategy();
- private volatile TransportProtos.SessionInfoProto sessionInfo;
- private volatile long lastActivityTime;
- private volatile long lastReportedActivityTime;
+ private AllEventsActivityStrategy() {
+ }
+
+ public static AllEventsActivityStrategy getInstance() {
+ return INSTANCE;
+ }
- SessionActivityData(TransportProtos.SessionInfoProto sessionInfo) {
- this.sessionInfo = sessionInfo;
+ @Override
+ public boolean onActivity() {
+ return true;
}
- void updateLastActivityTime() {
- this.lastActivityTime = System.currentTimeMillis();
+ @Override
+ public boolean onReportingPeriodEnd() {
+ return true;
}
}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategy.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategy.java
new file mode 100644
index 0000000000..a5a59046ae
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategy.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity.strategy;
+
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode
+public final class FirstAndLastEventActivityStrategy implements ActivityStrategy {
+
+ private boolean firstEventReceived;
+
+ @Override
+ public synchronized boolean onActivity() {
+ if (!firstEventReceived) {
+ firstEventReceived = true;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public synchronized boolean onReportingPeriodEnd() {
+ firstEventReceived = false;
+ return true;
+ }
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategy.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategy.java
new file mode 100644
index 0000000000..5353635c45
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategy.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity.strategy;
+
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode
+public final class FirstEventActivityStrategy implements ActivityStrategy {
+
+ private boolean firstEventReceived;
+
+ @Override
+ public synchronized boolean onActivity() {
+ if (!firstEventReceived) {
+ firstEventReceived = true;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public synchronized boolean onReportingPeriodEnd() {
+ firstEventReceived = false;
+ return false;
+ }
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/LastEventActivityStrategy.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/LastEventActivityStrategy.java
new file mode 100644
index 0000000000..bce35b0b34
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/activity/strategy/LastEventActivityStrategy.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity.strategy;
+
+public final class LastEventActivityStrategy implements ActivityStrategy {
+
+ private static final LastEventActivityStrategy INSTANCE = new LastEventActivityStrategy();
+
+ private LastEventActivityStrategy() {
+ }
+
+ public static LastEventActivityStrategy getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean onActivity() {
+ return false;
+ }
+
+ @Override
+ public boolean onReportingPeriodEnd() {
+ return true;
+ }
+
+}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
index 37b77a5268..0d7fbfc332 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
@@ -50,9 +50,9 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.limit.LimitedApi;
+import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@@ -96,9 +96,9 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.AsyncCallbackTemplate;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
-import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
+import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
@@ -109,16 +109,13 @@ import org.thingsboard.server.queue.util.TbTransportComponent;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Collections;
-import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -133,14 +130,12 @@ import java.util.stream.Collectors;
@Slf4j
@Service
@TbTransportComponent
-public class DefaultTransportService implements TransportService {
+public class DefaultTransportService extends TransportActivityManager implements TransportService {
public static final String OVERWRITE_ACTIVITY_TIME = "overwriteActivityTime";
- public static final String SESSION_EXPIRED_MESSAGE = "Session has expired due to last activity time!";
- public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_OPEN = getSessionEventMsg(TransportProtos.SessionEvent.OPEN);
- public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_CLOSED = getSessionEventMsg(TransportProtos.SessionEvent.CLOSED);
- public static final TransportProtos.SessionCloseNotificationProto SESSION_CLOSE_NOTIFICATION_PROTO = TransportProtos.SessionCloseNotificationProto.newBuilder()
- .setMessage(SESSION_EXPIRED_MESSAGE).build();
+ public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_OPEN = TransportProtos.SessionEventMsg.newBuilder()
+ .setSessionType(TransportProtos.SessionType.ASYNC)
+ .setEvent(TransportProtos.SessionEvent.OPEN).build();
public static final TransportProtos.SubscribeToAttributeUpdatesMsg SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG = TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC).build();
public static final TransportProtos.SubscribeToRPCMsg SUBSCRIBE_TO_RPC_ASYNC_MSG = TransportProtos.SubscribeToRPCMsg.newBuilder()
@@ -152,10 +147,6 @@ public class DefaultTransportService implements TransportService {
private boolean logEnabled;
@Value("${transport.log.max_length:1024}")
private int logMaxLength;
- @Value("${transport.sessions.inactivity_timeout}")
- private long sessionInactivityTimeout;
- @Value("${transport.sessions.report_timeout}")
- private long sessionReportTimeout;
@Value("${transport.client_side_rpc.timeout:60000}")
private long clientSideRpcTimeout;
@Value("${queue.transport.poll_interval}")
@@ -182,11 +173,9 @@ public class DefaultTransportService implements TransportService {
private final TransportRateLimitService rateLimitService;
private final DataDecodingEncodingService dataDecodingEncodingService;
- private final SchedulerComponent scheduler;
private final ApplicationEventPublisher eventPublisher;
private final TransportResourceCache transportResourceCache;
private final NotificationRuleProcessor notificationRuleProcessor;
-
private final EntityLimitsCache entityLimitsCache;
protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate;
@@ -201,8 +190,6 @@ public class DefaultTransportService implements TransportService {
protected ExecutorService transportCallbackExecutor;
private ExecutorService mainConsumerExecutor;
- public final ConcurrentMap sessions = new ConcurrentHashMap<>();
- private final ConcurrentMap sessionsActivity = new ConcurrentHashMap<>();
private final Map toServerRpcPendingMap = new ConcurrentHashMap<>();
private volatile boolean stopped = false;
@@ -238,11 +225,11 @@ public class DefaultTransportService implements TransportService {
@PostConstruct
public void init() {
+ super.init();
this.ruleEngineProducerStats = statsFactory.createMessagesStats(StatsType.RULE_ENGINE.getName() + ".producer");
this.tbCoreProducerStats = statsFactory.createMessagesStats(StatsType.CORE.getName() + ".producer");
this.transportApiStats = statsFactory.createMessagesStats(StatsType.TRANSPORT.getName() + ".producer");
this.transportCallbackExecutor = ThingsBoardExecutors.newWorkStealingPool(20, getClass());
- this.scheduler.scheduleAtFixedRate(this::checkInactivityAndReportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS);
this.scheduler.scheduleAtFixedRate(this::invalidateRateLimits, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS);
transportApiRequestTemplate = queueProvider.createTransportApiRequestTemplate();
transportApiRequestTemplate.setMessagesStats(transportApiStats);
@@ -558,7 +545,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
.setSessionEvent(msg).build(), callback);
}
@@ -578,7 +565,7 @@ public class DefaultTransportService implements TransportService {
}
}
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, msg, callback);
}
}
@@ -595,7 +582,7 @@ public class DefaultTransportService implements TransportService {
dataPoints += tsKv.getKvCount();
}
if (checkLimits(sessionInfo, msg, callback, dataPoints)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
TenantId tenantId = getTenantId(sessionInfo);
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
CustomerId customerId = getCustomerId(sessionInfo);
@@ -619,7 +606,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TbMsgMetaData md, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback, msg.getKvCount())) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
TenantId tenantId = getTenantId(sessionInfo);
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
@@ -639,7 +626,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.GetAttributeRequestMsg msg, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
.setGetAttributes(msg).build(), new ApiStatsProxyCallback<>(getTenantId(sessionInfo), getCustomerId(sessionInfo), 1, callback));
}
@@ -652,7 +639,7 @@ public class DefaultTransportService implements TransportService {
if (sessionMetaData != null) {
sessionMetaData.setSubscribedToAttributes(!msg.getUnsubscribe());
}
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscribeToAttributes(msg).build(),
new ApiStatsProxyCallback<>(getTenantId(sessionInfo), getCustomerId(sessionInfo), 1, callback));
}
@@ -665,7 +652,7 @@ public class DefaultTransportService implements TransportService {
if (sessionMetaData != null) {
sessionMetaData.setSubscribedToRPC(!msg.getUnsubscribe());
}
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setSubscribeToRPC(msg).build(),
new ApiStatsProxyCallback<>(getTenantId(sessionInfo), getCustomerId(sessionInfo), 1, callback));
}
@@ -674,7 +661,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToDeviceRpcResponseMsg msg, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setToDeviceRPCCallResponse(msg).build(),
new ApiStatsProxyCallback<>(getTenantId(sessionInfo), getCustomerId(sessionInfo), 1, callback));
}
@@ -683,7 +670,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void notifyAboutUplink(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg msg, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setUplinkNotificationMsg(msg).build(), callback);
}
}
@@ -704,7 +691,7 @@ public class DefaultTransportService implements TransportService {
if (checkLimits(sessionInfo, responseMsg, callback)) {
if (reportActivity) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
}
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setRpcResponseStatusMsg(responseMsg).build(),
new ApiStatsProxyCallback<>(getTenantId(sessionInfo), getCustomerId(sessionInfo), 1, TransportServiceCallback.EMPTY));
@@ -736,7 +723,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToServerRpcRequestMsg msg, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
UUID sessionId = toSessionId(sessionInfo);
TenantId tenantId = getTenantId(sessionInfo);
DeviceId deviceId = getDeviceId(sessionInfo);
@@ -761,7 +748,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ClaimDeviceMsg msg, TransportServiceCallback callback) {
if (checkLimits(sessionInfo, msg, callback)) {
- reportActivityInternal(sessionInfo);
+ recordActivityInternal(sessionInfo);
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
.setClaimDevice(msg).build(), callback);
}
@@ -780,69 +767,12 @@ public class DefaultTransportService implements TransportService {
}
@Override
- public void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
- reportActivityInternal(sessionInfo);
+ public void recordActivity(TransportProtos.SessionInfoProto sessionInfo) {
+ recordActivityInternal(sessionInfo);
}
- private void reportActivityInternal(TransportProtos.SessionInfoProto sessionInfo) {
- UUID sessionId = toSessionId(sessionInfo);
- SessionActivityData sessionMetaData = sessionsActivity.computeIfAbsent(sessionId, id -> new SessionActivityData(sessionInfo));
- sessionMetaData.updateLastActivityTime();
- }
-
- private void checkInactivityAndReportActivity() {
- long expTime = System.currentTimeMillis() - sessionInactivityTimeout;
- Set sessionsToRemove = new HashSet<>();
- sessionsActivity.forEach((uuid, sessionAD) -> {
- long lastActivityTime = sessionAD.getLastActivityTime();
- SessionMetaData sessionMD = sessions.get(uuid);
- if (sessionMD != null) {
- sessionAD.setSessionInfo(sessionMD.getSessionInfo());
- } else {
- sessionsToRemove.add(uuid);
- }
- TransportProtos.SessionInfoProto sessionInfo = sessionAD.getSessionInfo();
-
- if (sessionInfo.getGwSessionIdMSB() != 0 && sessionInfo.getGwSessionIdLSB() != 0) {
- var gwSessionId = new UUID(sessionInfo.getGwSessionIdMSB(), sessionInfo.getGwSessionIdLSB());
- SessionMetaData gwMetaData = sessions.get(gwSessionId);
- SessionActivityData gwActivityData = sessionsActivity.get(gwSessionId);
- if (gwMetaData != null && gwMetaData.isOverwriteActivityTime()) {
- lastActivityTime = Math.max(gwActivityData.getLastActivityTime(), lastActivityTime);
- }
- }
- if (lastActivityTime < expTime) {
- if (sessionMD != null) {
- if (log.isDebugEnabled()) {
- log.debug("[{}] Session has expired due to last activity time: {}", toSessionId(sessionInfo), lastActivityTime);
- }
- sessions.remove(uuid);
- sessionsToRemove.add(uuid);
- process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
- sessionMD.getListener().onRemoteSessionCloseCommand(uuid, SESSION_CLOSE_NOTIFICATION_PROTO);
- }
- } else {
- if (lastActivityTime > sessionAD.getLastReportedActivityTime()) {
- final long lastActivityTimeFinal = lastActivityTime;
- process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
- .setAttributeSubscription(sessionMD != null && sessionMD.isSubscribedToAttributes())
- .setRpcSubscription(sessionMD != null && sessionMD.isSubscribedToRPC())
- .setLastActivityTime(lastActivityTime).build(), new TransportServiceCallback() {
- @Override
- public void onSuccess(Void msg) {
- sessionAD.setLastReportedActivityTime(lastActivityTimeFinal);
- }
-
- @Override
- public void onError(Throwable e) {
- log.warn("[{}] Failed to report last activity time", uuid, e);
- }
- });
- }
- }
- });
- // Removes all closed or short-lived sessions.
- sessionsToRemove.forEach(sessionsActivity::remove);
+ private void recordActivityInternal(TransportProtos.SessionInfoProto sessionInfo) {
+ onActivity(toSessionId(sessionInfo), sessionInfo, getCurrentTimeMillis());
}
@Override
@@ -913,7 +843,7 @@ public class DefaultTransportService implements TransportService {
}
TransportProtos.PostTelemetryMsg.Builder request = TransportProtos.PostTelemetryMsg.newBuilder();
TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder();
- builder.setTs(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) * 1000L + (atomicTs.getAndIncrement() % 1000));
+ builder.setTs(TimeUnit.MILLISECONDS.toSeconds(getCurrentTimeMillis()) * 1000L + (atomicTs.getAndIncrement() % 1000));
builder.addKv(TransportProtos.KeyValueProto.newBuilder()
.setKey("transportLog")
.setType(TransportProtos.KeyValueType.STRING_V)
@@ -1001,8 +931,8 @@ public class DefaultTransportService implements TransportService {
Optional profileOpt = dataDecodingEncodingService.decode(msg.getData().toByteArray());
if (profileOpt.isPresent()) {
Tenant tenant = profileOpt.get();
- partitionService.removeTenant(tenant.getId());
boolean updated = tenantProfileCache.put(tenant.getId(), tenant.getTenantProfileId());
+ partitionService.evictTenantInfo(tenant.getId());
if (updated) {
rateLimitService.update(tenant.getId());
}
@@ -1027,7 +957,9 @@ public class DefaultTransportService implements TransportService {
} else if (EntityType.TENANT_PROFILE.equals(entityType)) {
tenantProfileCache.remove(new TenantProfileId(entityUuid));
} else if (EntityType.TENANT.equals(entityType)) {
- rateLimitService.remove(TenantId.fromUUID(entityUuid));
+ TenantId tenantId = TenantId.fromUUID(entityUuid);
+ rateLimitService.remove(tenantId);
+ partitionService.removeTenant(tenantId);
} else if (EntityType.DEVICE.equals(entityType)) {
rateLimitService.remove(new DeviceId(entityUuid));
onDeviceDeleted(new DeviceId(entityUuid));
@@ -1160,12 +1092,6 @@ public class DefaultTransportService implements TransportService {
return new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
}
- private static TransportProtos.SessionEventMsg getSessionEventMsg(TransportProtos.SessionEvent event) {
- return TransportProtos.SessionEventMsg.newBuilder()
- .setSessionType(TransportProtos.SessionType.ASYNC)
- .setEvent(event).build();
- }
-
protected void sendToDeviceActor(TransportProtos.SessionInfoProto sessionInfo, TransportToDeviceActorMsg toDeviceActorMsg, TransportServiceCallback callback) {
ToCoreMsg toCoreMsg = ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build();
sendToCore(getTenantId(sessionInfo), getDeviceId(sessionInfo), toCoreMsg, getRoutingKey(sessionInfo), callback);
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportActivityManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportActivityManager.java
new file mode 100644
index 0000000000..0c9ebd0e4b
--- /dev/null
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportActivityManager.java
@@ -0,0 +1,138 @@
+/**
+ * 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.transport.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.thingsboard.server.common.transport.TransportService;
+import org.thingsboard.server.common.transport.TransportServiceCallback;
+import org.thingsboard.server.common.transport.activity.AbstractActivityManager;
+import org.thingsboard.server.common.transport.activity.ActivityReportCallback;
+import org.thingsboard.server.common.transport.activity.ActivityState;
+import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy;
+import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategyType;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@Slf4j
+public abstract class TransportActivityManager extends AbstractActivityManager implements TransportService {
+
+ public static final String SESSION_EXPIRED_MESSAGE = "Session has expired due to last activity time!";
+
+ public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_CLOSED = TransportProtos.SessionEventMsg.newBuilder()
+ .setSessionType(TransportProtos.SessionType.ASYNC)
+ .setEvent(TransportProtos.SessionEvent.CLOSED).build();
+ public static final TransportProtos.SessionCloseNotificationProto SESSION_EXPIRED_NOTIFICATION_PROTO = TransportProtos.SessionCloseNotificationProto.newBuilder()
+ .setMessage(SESSION_EXPIRED_MESSAGE).build();
+
+ public final ConcurrentMap sessions = new ConcurrentHashMap<>();
+
+ @Value("${transport.sessions.report_timeout}")
+ protected long sessionReportTimeout;
+
+ @Value("${transport.sessions.inactivity_timeout}")
+ protected long sessionInactivityTimeout;
+
+ @Value("${transport.activity.reporting_strategy:LAST}")
+ private ActivityStrategyType reportingStrategyType;
+
+ @Override
+ protected long getReportingPeriodMillis() {
+ return sessionReportTimeout;
+ }
+
+ @Override
+ protected ActivityStrategy getStrategy() {
+ return reportingStrategyType.toStrategy();
+ }
+
+ @Override
+ protected ActivityState updateState(UUID sessionId, ActivityState state) {
+ SessionMetaData session = sessions.get(sessionId);
+ if (session == null) {
+ return null;
+ }
+
+ state.setMetadata(session.getSessionInfo());
+ var sessionInfo = state.getMetadata();
+
+ if (sessionInfo.getGwSessionIdMSB() == 0L || sessionInfo.getGwSessionIdLSB() == 0L) {
+ return state;
+ }
+
+ var gwSessionId = new UUID(sessionInfo.getGwSessionIdMSB(), sessionInfo.getGwSessionIdLSB());
+ SessionMetaData gwSession = sessions.get(gwSessionId);
+ if (gwSession == null || !gwSession.isOverwriteActivityTime()) {
+ return state;
+ }
+
+ long lastRecordedTime = state.getLastRecordedTime();
+ long gwLastRecordedTime = getLastRecordedTime(gwSessionId);
+ log.debug("Session with id: [{}] has gateway session with id: [{}] with overwrite activity time enabled. " +
+ "Updating last activity time. Session last recorded time: [{}], gateway session last recorded time: [{}].",
+ sessionId, gwSessionId, lastRecordedTime, gwLastRecordedTime);
+ state.setLastRecordedTime(Math.max(lastRecordedTime, gwLastRecordedTime));
+ return state;
+ }
+
+ @Override
+ protected boolean hasExpired(long lastRecordedTime) {
+ return (getCurrentTimeMillis() - sessionInactivityTimeout) > lastRecordedTime;
+ }
+
+ @Override
+ protected void onStateExpiry(UUID sessionId, TransportProtos.SessionInfoProto sessionInfo) {
+ log.debug("Session with id: [{}] has expired due to last activity time.", sessionId);
+ SessionMetaData expiredSession = sessions.remove(sessionId);
+ if (expiredSession != null) {
+ deregisterSession(sessionInfo);
+ process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
+ expiredSession.getListener().onRemoteSessionCloseCommand(sessionId, SESSION_EXPIRED_NOTIFICATION_PROTO);
+ }
+ }
+
+ @Override
+ protected void reportActivity(UUID sessionId, TransportProtos.SessionInfoProto currentSessionInfo, long timeToReport, ActivityReportCallback callback) {
+ log.debug("Reporting activity state for session with id: [{}]. Time to report: [{}].", sessionId, timeToReport);
+ SessionMetaData session = sessions.get(sessionId);
+ TransportProtos.SubscriptionInfoProto subscriptionInfo = TransportProtos.SubscriptionInfoProto.newBuilder()
+ .setAttributeSubscription(session != null && session.isSubscribedToAttributes())
+ .setRpcSubscription(session != null && session.isSubscribedToRPC())
+ .setLastActivityTime(timeToReport)
+ .build();
+ TransportProtos.SessionInfoProto sessionInfo = session != null ? session.getSessionInfo() : currentSessionInfo;
+ process(sessionInfo, subscriptionInfo, new TransportServiceCallback<>() {
+ @Override
+ public void onSuccess(Void msgAcknowledged) {
+ callback.onSuccess(sessionId, timeToReport);
+
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ callback.onFailure(sessionId, e);
+ }
+ });
+ }
+
+ protected long getCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+}
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategyTypeTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategyTypeTest.java
new file mode 100644
index 0000000000..7dcf0e8fd2
--- /dev/null
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/ActivityStrategyTypeTest.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.activity.strategy;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ActivityStrategyTypeTest {
+
+ @Test
+ public void testCreateAllEventsStrategy() {
+ assertThat(ActivityStrategyType.ALL.toStrategy()).isEqualTo(AllEventsActivityStrategy.getInstance());
+ }
+
+ @Test
+ public void testCreateFirstEventStrategy() {
+ assertThat(ActivityStrategyType.FIRST.toStrategy()).isEqualTo(new FirstEventActivityStrategy());
+ }
+
+ @Test
+ public void testCreateLastEventStrategy() {
+ assertThat(ActivityStrategyType.LAST.toStrategy()).isEqualTo(LastEventActivityStrategy.getInstance());
+ }
+
+ @Test
+ public void testCreateFirstAndLastEventStrategy() {
+ assertThat(ActivityStrategyType.FIRST_AND_LAST.toStrategy()).isEqualTo(new FirstAndLastEventActivityStrategy());
+ }
+
+}
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/AllEventsActivityStrategyTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/AllEventsActivityStrategyTest.java
new file mode 100644
index 0000000000..ea89b28a02
--- /dev/null
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/AllEventsActivityStrategyTest.java
@@ -0,0 +1,42 @@
+/**
+ * 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.transport.activity.strategy;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AllEventsActivityStrategyTest {
+
+ private AllEventsActivityStrategy strategy;
+
+ @BeforeEach
+ public void setUp() {
+ strategy = AllEventsActivityStrategy.getInstance();
+ }
+
+ @Test
+ public void testOnActivity() {
+ assertTrue(strategy.onActivity(), "onActivity() should always return true.");
+ }
+
+ @Test
+ public void testOnReportingPeriodEnd() {
+ assertTrue(strategy.onReportingPeriodEnd(), "onReportingPeriodEnd() should always return true.");
+ }
+
+}
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategyTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategyTest.java
new file mode 100644
index 0000000000..9ca6b58502
--- /dev/null
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/FirstAndLastEventActivityStrategyTest.java
@@ -0,0 +1,53 @@
+/**
+ * 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.transport.activity.strategy;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FirstAndLastEventActivityStrategyTest {
+
+ private FirstAndLastEventActivityStrategy strategy;
+
+ @BeforeEach
+ public void setUp() {
+ strategy = new FirstAndLastEventActivityStrategy();
+ }
+
+ @Test
+ public void testOnActivity_FirstCall() {
+ assertTrue(strategy.onActivity(), "First call of onActivity() should return true.");
+ }
+
+ @Test
+ public void testOnActivity_SubsequentCalls() {
+ assertTrue(strategy.onActivity(), "First call of onActivity() should return true.");
+ assertFalse(strategy.onActivity(), "Subsequent calls of onActivity() should return false.");
+ }
+
+ @Test
+ public void testOnReportingPeriodEnd() {
+ assertTrue(strategy.onActivity(), "First call of onActivity() should return true.");
+ assertTrue(strategy.onReportingPeriodEnd(), "onReportingPeriodEnd() should always return true.");
+ assertTrue(strategy.onActivity(), "onActivity() should return true after onReportingPeriodEnd() for the next reporting period");
+ assertTrue(strategy.onReportingPeriodEnd(), "onReportingPeriodEnd() should always return true.");
+ }
+
+
+}
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategyTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategyTest.java
new file mode 100644
index 0000000000..0df3517098
--- /dev/null
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/FirstEventActivityStrategyTest.java
@@ -0,0 +1,52 @@
+/**
+ * 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.transport.activity.strategy;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FirstEventActivityStrategyTest {
+
+ private FirstEventActivityStrategy strategy;
+
+ @BeforeEach
+ public void setUp() {
+ strategy = new FirstEventActivityStrategy();
+ }
+
+ @Test
+ public void testOnActivity_FirstCall() {
+ assertTrue(strategy.onActivity(), "First call of onActivity() should return true.");
+ }
+
+ @Test
+ public void testOnActivity_SubsequentCalls() {
+ assertTrue(strategy.onActivity(), "First call of onActivity() should return true.");
+ assertFalse(strategy.onActivity(), "Subsequent calls of onActivity() should return false.");
+ }
+
+ @Test
+ public void testOnReportingPeriodEnd() {
+ assertTrue(strategy.onActivity(), "First call of onActivity() should return true.");
+ assertFalse(strategy.onReportingPeriodEnd(), "onReportingPeriodEnd() should always return false.");
+ assertTrue(strategy.onActivity(), "onActivity() should return true after onReportingPeriodEnd().");
+ assertFalse(strategy.onReportingPeriodEnd(), "onReportingPeriodEnd() should always return false.");
+ }
+
+}
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/LastEventActivityStrategyTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/LastEventActivityStrategyTest.java
new file mode 100644
index 0000000000..d747c13787
--- /dev/null
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/activity/strategy/LastEventActivityStrategyTest.java
@@ -0,0 +1,43 @@
+/**
+ * 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.transport.activity.strategy;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LastEventActivityStrategyTest {
+
+ private LastEventActivityStrategy strategy;
+
+ @BeforeEach
+ public void setUp() {
+ strategy = LastEventActivityStrategy.getInstance();
+ }
+
+ @Test
+ public void testOnActivity() {
+ assertFalse(strategy.onActivity(), "onActivity() should always return false.");
+ }
+
+ @Test
+ public void testOnReportingPeriodEnd() {
+ assertTrue(strategy.onReportingPeriodEnd(), "onReportingPeriodEnd() should always return true.");
+ }
+
+}
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java
new file mode 100644
index 0000000000..a87532541d
--- /dev/null
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java
@@ -0,0 +1,517 @@
+/**
+ * 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.transport.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+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.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+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.transport.SessionMsgListener;
+import org.thingsboard.server.common.transport.TransportServiceCallback;
+import org.thingsboard.server.common.transport.activity.ActivityReportCallback;
+import org.thingsboard.server.common.transport.activity.ActivityState;
+import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy;
+import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategyType;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED;
+import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EXPIRED_NOTIFICATION_PROTO;
+
+@ExtendWith(MockitoExtension.class)
+public class TransportActivityManagerTest {
+
+ private final UUID SESSION_ID = UUID.fromString("1306648a-9b26-11ee-b9d1-0242ac120002");
+
+ @Mock
+ private DefaultTransportService transportServiceMock;
+ private ConcurrentMap sessions;
+
+ @BeforeEach
+ public void setup() {
+ sessions = new ConcurrentHashMap<>();
+ ReflectionTestUtils.setField(transportServiceMock, "sessions", sessions);
+ }
+
+ @Test
+ void givenFirstActivityForAlreadyRemovedSessionAndFirstEventReportingStrategy_whenOnActivity_thenShouldRecordActivityAndReport() {
+ // GIVEN
+ ConcurrentMap states = new ConcurrentHashMap<>();
+ ReflectionTestUtils.setField(transportServiceMock, "states", states);
+
+ var strategyMock = mock(ActivityStrategy.class);
+ when(transportServiceMock.getStrategy()).thenReturn(strategyMock);
+ when(strategyMock.onActivity()).thenReturn(true);
+
+ long activityTime = 123L;
+ var sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+
+ doCallRealMethod().when(transportServiceMock).getLastRecordedTime(SESSION_ID);
+ doCallRealMethod().when(transportServiceMock).onActivity(SESSION_ID, sessionInfo, activityTime);
+
+ // WHEN
+ transportServiceMock.onActivity(SESSION_ID, sessionInfo, activityTime);
+
+ // THEN
+ assertThat(states).containsKey(SESSION_ID);
+ assertThat(transportServiceMock.getLastRecordedTime(SESSION_ID)).isEqualTo(activityTime);
+ verify(transportServiceMock).reportActivity(eq(SESSION_ID), eq(sessionInfo), eq(activityTime), any(ActivityReportCallback.class));
+ }
+
+ @Test
+ void givenKeyAndTimeToReportAndSessionExists_whenReportingActivity_thenShouldReportActivityWithSubscriptionsAndSessionInfoFromSession() {
+ // GIVEN
+ long expectedTime = 123L;
+ boolean expectedAttributesSubscription = true;
+ boolean expectedRPCSubscription = true;
+ TransportProtos.SessionInfoProto expectedSessionInfo = TransportProtos.SessionInfoProto.getDefaultInstance();
+
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ SessionMetaData session = new SessionMetaData(expectedSessionInfo, TransportProtos.SessionType.ASYNC, listenerMock);
+ session.setSubscribedToAttributes(expectedAttributesSubscription);
+ session.setSubscribedToRPC(expectedRPCSubscription);
+ sessions.put(SESSION_ID, session);
+
+ ActivityReportCallback callbackMock = mock(ActivityReportCallback.class);
+
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+
+ doCallRealMethod().when(transportServiceMock).reportActivity(SESSION_ID, sessionInfo, expectedTime, callbackMock);
+
+ // WHEN
+ transportServiceMock.reportActivity(SESSION_ID, sessionInfo, expectedTime, callbackMock);
+
+ // THEN
+ ArgumentCaptor sessionInfoCaptor = ArgumentCaptor.forClass(TransportProtos.SessionInfoProto.class);
+ ArgumentCaptor subscriptionInfoCaptor = ArgumentCaptor.forClass(TransportProtos.SubscriptionInfoProto.class);
+ ArgumentCaptor> callbackCaptor = ArgumentCaptor.forClass(TransportServiceCallback.class);
+
+ verify(transportServiceMock).process(sessionInfoCaptor.capture(), subscriptionInfoCaptor.capture(), callbackCaptor.capture());
+
+ assertThat(sessionInfoCaptor.getValue()).isEqualTo(expectedSessionInfo);
+
+ TransportProtos.SubscriptionInfoProto expectedSubscriptionInfo = TransportProtos.SubscriptionInfoProto.newBuilder()
+ .setAttributeSubscription(expectedAttributesSubscription)
+ .setRpcSubscription(expectedRPCSubscription)
+ .setLastActivityTime(expectedTime)
+ .build();
+ assertThat(subscriptionInfoCaptor.getValue()).isEqualTo(expectedSubscriptionInfo);
+
+ TransportServiceCallback queueCallback = callbackCaptor.getValue();
+
+ queueCallback.onSuccess(null);
+ verify(callbackMock).onSuccess(SESSION_ID, expectedTime);
+
+ var throwable = new Throwable();
+ queueCallback.onError(throwable);
+ verify(callbackMock).onFailure(SESSION_ID, throwable);
+ }
+
+ @Test
+ void givenKeyAndTimeToReportAndSessionDoesNotExist_whenReportingActivity_thenShouldReportActivityWithNoSubscriptionsAndPreviousSessionInfo() {
+ // GIVEN
+ long expectedTime = 123L;
+ boolean expectedAttributesSubscription = false;
+ boolean expectedRPCSubscription = false;
+ TransportProtos.SessionInfoProto expectedSessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+
+ ActivityReportCallback callbackMock = mock(ActivityReportCallback.class);
+
+ doCallRealMethod().when(transportServiceMock).reportActivity(SESSION_ID, expectedSessionInfo, expectedTime, callbackMock);
+
+ // WHEN
+ transportServiceMock.reportActivity(SESSION_ID, expectedSessionInfo, expectedTime, callbackMock);
+
+ // THEN
+ ArgumentCaptor sessionInfoCaptor = ArgumentCaptor.forClass(TransportProtos.SessionInfoProto.class);
+ ArgumentCaptor subscriptionInfoCaptor = ArgumentCaptor.forClass(TransportProtos.SubscriptionInfoProto.class);
+ ArgumentCaptor> callbackCaptor = ArgumentCaptor.forClass(TransportServiceCallback.class);
+
+ verify(transportServiceMock).process(sessionInfoCaptor.capture(), subscriptionInfoCaptor.capture(), callbackCaptor.capture());
+
+ assertThat(sessionInfoCaptor.getValue()).isEqualTo(expectedSessionInfo);
+
+ TransportProtos.SubscriptionInfoProto expectedSubscriptionInfo = TransportProtos.SubscriptionInfoProto.newBuilder()
+ .setAttributeSubscription(expectedAttributesSubscription)
+ .setRpcSubscription(expectedRPCSubscription)
+ .setLastActivityTime(expectedTime)
+ .build();
+ assertThat(subscriptionInfoCaptor.getValue()).isEqualTo(expectedSubscriptionInfo);
+
+ TransportServiceCallback queueCallback = callbackCaptor.getValue();
+
+ queueCallback.onSuccess(null);
+ verify(callbackMock).onSuccess(SESSION_ID, expectedTime);
+
+ var throwable = new Throwable();
+ queueCallback.onError(throwable);
+ verify(callbackMock).onFailure(SESSION_ID, throwable);
+ }
+
+ @Test
+ void givenActivityHappened_whenRecordActivity_thenShouldDelegateToOnActivity() {
+ // GIVEN
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+ doCallRealMethod().when(transportServiceMock).recordActivity(sessionInfo);
+ when(transportServiceMock.toSessionId(sessionInfo)).thenReturn(SESSION_ID);
+ long expectedTime = 123L;
+ when(transportServiceMock.getCurrentTimeMillis()).thenReturn(expectedTime);
+
+ // WHEN
+ transportServiceMock.recordActivity(sessionInfo);
+
+ // THEN
+ verify(transportServiceMock).onActivity(SESSION_ID, sessionInfo, expectedTime);
+ }
+
+ @ParameterizedTest
+ @EnumSource(ActivityStrategyType.class)
+ void givenDifferentReportingStrategies_whenGettingStrategy_thenShouldReturnCorrectStrategy(ActivityStrategyType reportingStrategyType) {
+ // GIVEN
+ doCallRealMethod().when(transportServiceMock).getStrategy();
+ ReflectionTestUtils.setField(transportServiceMock, "reportingStrategyType", reportingStrategyType);
+
+ // WHEN
+ ActivityStrategy actualStrategy = transportServiceMock.getStrategy();
+
+ // THEN
+ assertThat(actualStrategy).isEqualTo(reportingStrategyType.toStrategy());
+ }
+
+ @Test
+ void givenSessionDoesNotExist_whenUpdatingActivityState_thenShouldReturnNull() {
+ // GIVEN
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+
+ ActivityState state = new ActivityState<>();
+ state.setLastRecordedTime(123L);
+ state.setMetadata(sessionInfo);
+
+ when(transportServiceMock.updateState(SESSION_ID, state)).thenCallRealMethod();
+
+ // WHEN
+ ActivityState updatedState = transportServiceMock.updateState(SESSION_ID, state);
+
+ // THEN
+ assertThat(updatedState).isNull();
+ }
+
+ @Test
+ void givenNoGwSessionId_whenUpdatingActivityState_thenShouldReturnSameInstanceWithUpdatedSessionInfo() {
+ // GIVEN
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ sessions.put(SESSION_ID, new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listenerMock));
+
+ long lastRecordedTime = 123L;
+
+ ActivityState state = new ActivityState<>();
+ state.setLastRecordedTime(lastRecordedTime);
+ state.setMetadata(TransportProtos.SessionInfoProto.getDefaultInstance());
+
+ when(transportServiceMock.updateState(SESSION_ID, state)).thenCallRealMethod();
+
+ // WHEN
+ ActivityState updatedState = transportServiceMock.updateState(SESSION_ID, state);
+
+ // THEN
+ assertThat(updatedState).isSameAs(state);
+ assertThat(updatedState.getLastRecordedTime()).isEqualTo(lastRecordedTime);
+ assertThat(updatedState.getMetadata()).isEqualTo(sessionInfo);
+ }
+
+ @Test
+ void givenHasGwSessionIdButGwSessionIsNotNull_whenUpdatingActivityState_thenShouldReturnSameInstanceWithUpdatedSessionInfo() {
+ // GIVEN
+ var gwSessionId = UUID.fromString("19864038-9b48-11ee-b9d1-0242ac120002");
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .setGwSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setGwSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ sessions.put(SESSION_ID, new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listenerMock));
+
+ long lastRecordedTime = 123L;
+
+ ActivityState state = new ActivityState<>();
+ state.setLastRecordedTime(lastRecordedTime);
+ state.setMetadata(TransportProtos.SessionInfoProto.getDefaultInstance());
+
+ when(transportServiceMock.updateState(SESSION_ID, state)).thenCallRealMethod();
+
+ // WHEN
+ ActivityState updatedState = transportServiceMock.updateState(SESSION_ID, state);
+
+ // THEN
+ assertThat(updatedState).isSameAs(state);
+ assertThat(updatedState.getLastRecordedTime()).isEqualTo(lastRecordedTime);
+ assertThat(updatedState.getMetadata()).isEqualTo(sessionInfo);
+
+ verify(transportServiceMock, never()).getLastRecordedTime(gwSessionId);
+ }
+
+ @Test
+ void givenHasGwSessionWithoutOverwriteEnabled_whenUpdatingActivityState_thenShouldReturnSameInstanceWithUpdatedSessionInfo() {
+ // GIVEN
+ var gwSessionId = UUID.fromString("19864038-9b48-11ee-b9d1-0242ac120002");
+ TransportProtos.SessionInfoProto gwSessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener gwListenerMock = mock(SessionMsgListener.class);
+ sessions.put(gwSessionId, new SessionMetaData(gwSessionInfo, TransportProtos.SessionType.ASYNC, gwListenerMock));
+
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .setGwSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setGwSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ sessions.put(SESSION_ID, new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listenerMock));
+
+ long lastRecordedTime = 123L;
+
+ ActivityState state = new ActivityState<>();
+ state.setLastRecordedTime(lastRecordedTime);
+ state.setMetadata(TransportProtos.SessionInfoProto.getDefaultInstance());
+
+ when(transportServiceMock.updateState(SESSION_ID, state)).thenCallRealMethod();
+
+ // WHEN
+ ActivityState updatedState = transportServiceMock.updateState(SESSION_ID, state);
+
+ // THEN
+ assertThat(updatedState).isSameAs(state);
+ assertThat(updatedState.getLastRecordedTime()).isEqualTo(lastRecordedTime);
+ assertThat(updatedState.getMetadata()).isEqualTo(sessionInfo);
+
+ verify(transportServiceMock, never()).getLastRecordedTime(gwSessionId);
+ }
+
+ @Test
+ void givenHasGwSessionWithOverwriteEnabledAndGwLastRecordedTimeIsGreater_whenUpdatingActivityState_thenShouldReturnSameInstanceWithUpdatedSessionInfoAndLastRecordedTime() {
+ // GIVEN
+ var gwSessionId = UUID.fromString("19864038-9b48-11ee-b9d1-0242ac120002");
+ TransportProtos.SessionInfoProto gwSessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener gwListenerMock = mock(SessionMsgListener.class);
+ SessionMetaData gwSession = new SessionMetaData(gwSessionInfo, TransportProtos.SessionType.ASYNC, gwListenerMock);
+ gwSession.setOverwriteActivityTime(true);
+ sessions.put(gwSessionId, gwSession);
+
+ long gwLastRecordedTime = 500L;
+ when(transportServiceMock.getLastRecordedTime(gwSessionId)).thenReturn(gwLastRecordedTime);
+
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .setGwSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setGwSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ sessions.put(SESSION_ID, new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listenerMock));
+
+ long lastRecordedTime = 123L;
+
+ ActivityState state = new ActivityState<>();
+ state.setLastRecordedTime(lastRecordedTime);
+ state.setMetadata(TransportProtos.SessionInfoProto.getDefaultInstance());
+
+ when(transportServiceMock.updateState(SESSION_ID, state)).thenCallRealMethod();
+
+ // WHEN
+ ActivityState updatedState = transportServiceMock.updateState(SESSION_ID, state);
+
+ // THEN
+ assertThat(updatedState).isSameAs(state);
+ assertThat(updatedState.getLastRecordedTime()).isEqualTo(gwLastRecordedTime);
+ assertThat(updatedState.getMetadata()).isEqualTo(sessionInfo);
+ }
+
+ @Test
+ void givenHasGwSessionWithOverwriteEnabledAndGwLastRecordedTimeIsLess_whenUpdatingActivityState_thenShouldReturnSameInstanceWithUpdatedSessionInfoOnly() {
+ // GIVEN
+ var gwSessionId = UUID.fromString("19864038-9b48-11ee-b9d1-0242ac120002");
+ TransportProtos.SessionInfoProto gwSessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener gwListenerMock = mock(SessionMsgListener.class);
+ SessionMetaData gwSession = new SessionMetaData(gwSessionInfo, TransportProtos.SessionType.ASYNC, gwListenerMock);
+ gwSession.setOverwriteActivityTime(true);
+ sessions.put(gwSessionId, gwSession);
+
+ long gwLastRecordedTime = 100L;
+ when(transportServiceMock.getLastRecordedTime(gwSessionId)).thenReturn(gwLastRecordedTime);
+
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .setGwSessionIdMSB(gwSessionId.getMostSignificantBits())
+ .setGwSessionIdLSB(gwSessionId.getLeastSignificantBits())
+ .build();
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ sessions.put(SESSION_ID, new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listenerMock));
+
+ long lastRecordedTime = 123L;
+
+ ActivityState state = new ActivityState<>();
+ state.setLastRecordedTime(lastRecordedTime);
+ state.setMetadata(TransportProtos.SessionInfoProto.getDefaultInstance());
+
+ when(transportServiceMock.updateState(SESSION_ID, state)).thenCallRealMethod();
+
+ // WHEN
+ ActivityState updatedState = transportServiceMock.updateState(SESSION_ID, state);
+
+ // THEN
+ assertThat(updatedState).isSameAs(state);
+ assertThat(updatedState.getLastRecordedTime()).isEqualTo(lastRecordedTime);
+ assertThat(updatedState.getMetadata()).isEqualTo(sessionInfo);
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideTestParamsForHasExpiredTrue")
+ public void givenExpiredLastRecordedTime_whenCheckingForExpiry_thenShouldReturnTrue(long currentTimeMillis, long lastRecordedTime, long sessionInactivityTimeout) {
+ // GIVEN
+ ReflectionTestUtils.setField(transportServiceMock, "sessionInactivityTimeout", sessionInactivityTimeout);
+
+ when(transportServiceMock.getCurrentTimeMillis()).thenReturn(currentTimeMillis);
+ when(transportServiceMock.hasExpired(lastRecordedTime)).thenCallRealMethod();
+
+ // WHEN
+ boolean hasExpired = transportServiceMock.hasExpired(lastRecordedTime);
+
+ // THEN
+ assertThat(hasExpired).isTrue();
+ }
+
+ private static Stream provideTestParamsForHasExpiredTrue() {
+ return Stream.of(
+ Arguments.of(10L, 0L, 9L),
+ Arguments.of(10L, 7L, 2L),
+ Arguments.of(10L, 8L, 1L),
+ Arguments.of(10000L, 5000L, 3000L)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideTestParamsForHasExpiredFalse")
+ public void givenNotExpiredLastRecordedTime_whenCheckingForExpiry_thenShouldReturnFalse(long currentTimeMillis, long lastRecordedTime, long sessionInactivityTimeout) {
+ // GIVEN
+ ReflectionTestUtils.setField(transportServiceMock, "sessionInactivityTimeout", sessionInactivityTimeout);
+
+ when(transportServiceMock.getCurrentTimeMillis()).thenReturn(currentTimeMillis);
+ when(transportServiceMock.hasExpired(lastRecordedTime)).thenCallRealMethod();
+
+ // WHEN
+ boolean hasExpired = transportServiceMock.hasExpired(lastRecordedTime);
+
+ // THEN
+ assertThat(hasExpired).isFalse();
+ }
+
+ private static Stream provideTestParamsForHasExpiredFalse() {
+ return Stream.of(
+ Arguments.of(10L, 9L, 2L),
+ Arguments.of(10L, 0L, 11L),
+ Arguments.of(10L, 8L, 3L),
+ Arguments.of(10000L, 8000L, 3000L)
+ );
+ }
+
+ @Test
+ void givenSessionExists_whenOnStateExpiryCalled_thenShouldPerformExpirationActions() {
+ // GIVEN
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+ SessionMsgListener listenerMock = mock(SessionMsgListener.class);
+ sessions.put(SESSION_ID, new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listenerMock));
+ doCallRealMethod().when(transportServiceMock).onStateExpiry(SESSION_ID, sessionInfo);
+
+ // WHEN
+ transportServiceMock.onStateExpiry(SESSION_ID, sessionInfo);
+
+ // THEN
+ assertThat(sessions.containsKey(SESSION_ID)).isFalse();
+ verify(transportServiceMock).deregisterSession(sessionInfo);
+ verify(transportServiceMock).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
+ verify(listenerMock).onRemoteSessionCloseCommand(SESSION_ID, SESSION_EXPIRED_NOTIFICATION_PROTO);
+ }
+
+ @Test
+ void givenSessionDoesNotExist_whenOnStateExpiryCalled_thenShouldNotPerformExpirationActions() {
+ // GIVEN
+ TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
+ .setSessionIdMSB(SESSION_ID.getMostSignificantBits())
+ .setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
+ .build();
+ doCallRealMethod().when(transportServiceMock).onStateExpiry(SESSION_ID, sessionInfo);
+
+ // WHEN
+ transportServiceMock.onStateExpiry(SESSION_ID, sessionInfo);
+
+ // THEN
+ assertThat(sessions.containsKey(SESSION_ID)).isFalse();
+ verify(transportServiceMock, never()).deregisterSession(sessionInfo);
+ verify(transportServiceMock, never()).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
+ }
+
+}
diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
index 60e75b5907..1d6688cebc 100644
--- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
+++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
@@ -160,7 +160,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
@Override
protected void onTbApplicationEvent(PartitionChangeEvent event) {
for (TenantId tenantId : vcService.getActiveRepositoryTenants()) {
- if (!partitionService.resolve(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId).isMyPartition()) {
+ if (!partitionService.isMyPartition(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId)) {
var lock = getRepoLock(tenantId);
lock.lock();
try {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java
index b1395ee34b..91b4f339b1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.alarm;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
-import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@@ -29,13 +28,10 @@ import java.util.UUID;
public interface AlarmCommentDao extends Dao {
- AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment);
-
- void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId);
-
AlarmComment findAlarmCommentById(TenantId tenantId, UUID key);
PageData findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink);
ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, UUID key);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
index 3bc8ceb1a0..ac6f4955d8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.alarm;
-import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
@@ -31,10 +30,10 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService;
+import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
+import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator;
-import java.util.UUID;
-
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@@ -50,18 +49,28 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
@Override
public AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) {
alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
- if (alarmComment.getId() == null) {
- return createAlarmComment(tenantId, alarmComment);
+ boolean isCreated = alarmComment.getId() == null;
+ AlarmComment result;
+ if (isCreated) {
+ result = createAlarmComment(tenantId, alarmComment);
} else {
- return updateAlarmComment(tenantId, alarmComment);
+ result = updateAlarmComment(tenantId, alarmComment);
+ }
+ if (result != null) {
+ eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(result)
+ .entityId(result.getAlarmId()).created(isCreated).build());
}
+ return result;
}
@Override
public AlarmComment saveAlarmComment(TenantId tenantId, AlarmComment alarmComment) {
log.debug("Deleting Alarm Comment: {}", alarmComment);
alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
- return alarmCommentDao.save(tenantId, alarmComment);
+ AlarmComment result = alarmCommentDao.save(tenantId, alarmComment);
+ eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(result)
+ .entityId(result.getAlarmId()).build());
+ return result;
}
@Override
@@ -89,12 +98,7 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
if (alarmComment.getType() == null) {
alarmComment.setType(AlarmCommentType.OTHER);
}
- if (alarmComment.getId() == null) {
- UUID uuid = Uuids.timeBased();
- alarmComment.setId(new AlarmCommentId(uuid));
- alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid));
- }
- return alarmCommentDao.createAlarmComment(tenantId, alarmComment);
+ return alarmCommentDao.save(tenantId, alarmComment);
}
private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) {
@@ -112,5 +116,4 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
}
return null;
}
-
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
index ae9763c59c..72e1b9c7bf 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -135,7 +135,7 @@ public class BaseAlarmService extends AbstractCachedEntityService findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds);
+
+ List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, EntityType entityType, List entityIds, String attributeType);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
index cba126d832..342cb26485 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -83,6 +84,15 @@ public class BaseAttributesService implements AttributesService {
return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
}
+ @Override
+ public List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds, String scope) {
+ if (StringUtils.isEmpty(scope)) {
+ return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
+ } else {
+ return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityType, entityIds, scope);
+ }
+ }
+
@Override
public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) {
validate(entityId, scope);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
index 6d31f6db1d..1666015a48 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
@@ -220,6 +220,15 @@ public class CachedAttributesService implements AttributesService {
return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
}
+ @Override
+ public List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds, String scope) {
+ if (StringUtils.isEmpty(scope)) {
+ return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
+ } else {
+ return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityType, entityIds, scope);
+ }
+ }
+
@Override
public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) {
validate(entityId, scope);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
index ba4b4283e7..f938aba5a9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
@@ -30,8 +30,6 @@ import java.util.UUID;
public interface AuditLogDao extends Dao {
- ListenableFuture saveByTenantId(AuditLog auditLog);
-
PageData findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink);
PageData findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 3252a481c4..4350f2a5d1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -15,11 +15,9 @@
*/
package org.thingsboard.server.dao.audit;
-import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -34,7 +32,6 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.AuditLogId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -50,11 +47,11 @@ import org.thingsboard.server.dao.audit.sink.AuditLogSink;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
-import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.Validator.validateEntityId;
@@ -66,7 +63,6 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
public class AuditLogServiceImpl implements AuditLogService {
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
- private static final int INSERTS_PER_ENTRY = 3;
@Autowired
private AuditLogLevelFilter auditLogLevelFilter;
@@ -80,6 +76,9 @@ public class AuditLogServiceImpl implements AuditLogService {
@Autowired
private AuditLogSink auditLogSink;
+ @Autowired
+ private JpaExecutorService executor;
+
@Autowired
private DataValidator auditLogValidator;
@@ -115,7 +114,7 @@ public class AuditLogServiceImpl implements AuditLogService {
}
@Override
- public ListenableFuture>
+ public ListenableFuture
logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
ActionType actionType, Exception e, Object... additionalInfo) {
if (canLog(entityId.getEntityType(), actionType)) {
@@ -370,9 +369,6 @@ public class AuditLogServiceImpl implements AuditLogService {
ActionStatus actionStatus,
String actionFailureDetails) {
AuditLog result = new AuditLog();
- UUID id = Uuids.timeBased();
- result.setId(new AuditLogId(id));
- result.setCreatedTime(Uuids.unixTimestamp(id));
result.setTenantId(tenantId);
result.setEntityId(entityId);
result.setEntityName(entityName);
@@ -386,16 +382,16 @@ public class AuditLogServiceImpl implements AuditLogService {
return result;
}
- private ListenableFuture> logAction(TenantId tenantId,
- EntityId entityId,
- String entityName,
- CustomerId customerId,
- UserId userId,
- String userName,
- ActionType actionType,
- JsonNode actionData,
- ActionStatus actionStatus,
- String actionFailureDetails) {
+ private ListenableFuture logAction(TenantId tenantId,
+ EntityId entityId,
+ String entityName,
+ CustomerId customerId,
+ UserId userId,
+ String userName,
+ ActionType actionType,
+ JsonNode actionData,
+ ActionStatus actionStatus,
+ String actionFailureDetails) {
AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName,
actionType, actionData, actionStatus, actionFailureDetails);
log.trace("Executing logAction [{}]", auditLogEntry);
@@ -408,12 +404,12 @@ public class AuditLogServiceImpl implements AuditLogService {
return Futures.immediateFailedFuture(e);
}
}
- List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
- futures.add(auditLogDao.saveByTenantId(auditLogEntry));
- auditLogSink.logAction(auditLogEntry);
-
- return Futures.allAsList(futures);
+ return executor.submit(() -> {
+ AuditLog auditLog = auditLogDao.save(tenantId, auditLogEntry);
+ auditLogSink.logAction(auditLog);
+ return null;
+ });
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
index 87ede9452a..28c00e4d58 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
@@ -55,7 +55,7 @@ public class DummyAuditLogServiceImpl implements AuditLogService {
}
@Override
- public ListenableFuture> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
+ public ListenableFuture logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
return null;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java
index 18e24b9094..f2dc272848 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java
@@ -34,14 +34,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.id.TenantId;
import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
@Component
@ConditionalOnProperty(prefix = "audit-log.sink", value = "type", havingValue = "elasticsearch")
@@ -68,6 +72,7 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
private String dateFormat;
private RestClient restClient;
+ private ExecutorService executor;
@PostConstruct
public void init() {
@@ -87,14 +92,32 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
}
this.restClient = builder.build();
+ this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("elasticsearch-audit-log"));
} catch (Exception e) {
log.error("Sink init failed!", e);
throw new RuntimeException(e.getMessage(), e);
}
}
+ @PreDestroy
+ private void destroy() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
@Override
public void logAction(AuditLog auditLogEntry) {
+ executor.execute(() -> {
+ try {
+ doLogAction(auditLogEntry);
+ } catch (Exception e) {
+ log.error("Failed to log action", e);
+ }
+ });
+ }
+
+ private void doLogAction(AuditLog auditLogEntry) {
String jsonContent = createElasticJsonRecord(auditLogEntry);
HttpEntity entity = new NStringEntity(
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
index bc9175c0a0..633333dcbd 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
@@ -113,7 +113,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
countService.publishCountEntityEvictEvent(savedCustomer.getTenantId(), EntityType.CUSTOMER);
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCustomer.getTenantId())
- .entityId(savedCustomer.getId()).added(customer.getId() == null).build());
+ .entityId(savedCustomer.getId()).created(customer.getId() == null).build());
return savedCustomer;
} catch (Exception e) {
checkConstraintViolation(e, "customer_external_id_unq_key", "Customer with such external id already exists!");
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index fb3bdce815..b60f356b0b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -161,7 +161,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
var saved = dashboardDao.save(dashboard.getTenantId(), dashboard);
publishEvictEvent(new DashboardTitleEvictEvent(saved.getId()));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId())
- .entityId(saved.getId()).added(dashboard.getId() == null).build());
+ .entityId(saved.getId()).created(dashboard.getId() == null).build());
if (dashboard.getId() == null) {
countService.publishCountEntityEvictEvent(saved.getTenantId(), EntityType.DASHBOARD);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java
index 66fc4421ba..29d7cab0a9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java
@@ -18,14 +18,15 @@ package org.thingsboard.server.dao.device;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import java.util.HashMap;
import java.util.Map;
+@Profile("install")
@Configuration
@ConfigurationProperties(prefix = "device")
@Data
-@Deprecated(since = "3.6.1")
public class DeviceConnectivityConfiguration {
private Map connectivity = new HashMap<>();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
index a922350486..403df3e822 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
@@ -191,7 +191,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService {
private final T entity;
private final T oldEntity;
private final EntityId entityId;
- private final Boolean added;
+ private final Boolean created;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 2bd943862c..2fe56f5d1f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -354,6 +354,7 @@ public class ModelConstants {
public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor";
public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_VERSION_PROPERTY = "configuration_version";
public static final String COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY = "actions";
+ public static final String COMPONENT_DESCRIPTOR_HAS_QUEUE_NAME_PROPERTY = "has_queue_name";
/**
* Event constants.
@@ -389,6 +390,7 @@ public class ModelConstants {
public static final String DEBUG_MODE = "debug_mode";
public static final String SINGLETON_MODE = "singleton_mode";
+ public static final String QUEUE_NAME = "queue_name";
/**
* Rule chain constants.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
index 84ee924041..8938fc5ad4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
@@ -70,6 +70,9 @@ public class ComponentDescriptorEntity extends BaseSqlEntity {
@Column(name = ModelConstants.SINGLETON_MODE)
private boolean singletonMode;
+ @Column(name = ModelConstants.QUEUE_NAME)
+ private String queueName;
+
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId;
@@ -84,6 +87,7 @@ public class RuleNodeEntity extends BaseSqlEntity {
this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
+ this.queueName = ruleNode.getQueueName();
this.configurationVersion = ruleNode.getConfigurationVersion();
this.configuration = ruleNode.getConfiguration();
this.additionalInfo = ruleNode.getAdditionalInfo();
@@ -103,6 +107,7 @@ public class RuleNodeEntity extends BaseSqlEntity {
ruleNode.setName(name);
ruleNode.setDebugMode(debugMode);
ruleNode.setSingletonMode(singletonMode);
+ ruleNode.setQueueName(queueName);
ruleNode.setConfigurationVersion(configurationVersion);
ruleNode.setConfiguration(configuration);
ruleNode.setAdditionalInfo(additionalInfo);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
index 24539828c9..53a585c358 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
@@ -84,7 +84,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService, D>
protected abstract JpaRepository getRepository();
- protected void setSearchText(E entity) {
- }
-
@Override
@Transactional
public D save(TenantId tenantId, D domain) {
@@ -58,17 +57,21 @@ public abstract class JpaAbstractDao, D>
log.error("Can't create entity for domain object {}", domain, e);
throw new IllegalArgumentException("Can't create entity for domain object {" + domain + "}", e);
}
- setSearchText(entity);
log.debug("Saving entity {}", entity);
- if (entity.getUuid() == null) {
+ boolean isNew = entity.getUuid() == null;
+ if (isNew) {
UUID uuid = Uuids.timeBased();
entity.setUuid(uuid);
entity.setCreatedTime(Uuids.unixTimestamp(uuid));
}
- entity = getRepository().save(entity);
+ entity = doSave(entity, isNew);
return DaoUtil.getData(entity);
}
+ protected E doSave(E entity, boolean isNew) {
+ return getRepository().save(entity);
+ }
+
@Override
@Transactional
public D saveAndFlush(TenantId tenantId, D domain) {
@@ -121,4 +124,5 @@ public abstract class JpaAbstractDao, D>
List entities = Lists.newArrayList(getRepository().findAll());
return DaoUtil.convertDataList(entities);
}
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java
new file mode 100644
index 0000000000..a933837f5d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java
@@ -0,0 +1,43 @@
+/**
+ * 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.dao.sql;
+
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+
+@SqlDao
+public abstract class JpaPartitionedAbstractDao, D> extends JpaAbstractDao {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Override
+ protected E doSave(E entity, boolean isNew) {
+ createPartition(entity);
+ if (isNew) {
+ entityManager.persist(entity);
+ } else {
+ entity = entityManager.merge(entity);
+ }
+ return entity;
+ }
+
+ public abstract void createPartition(E entity);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java
index 1f83211b79..3d366a5a56 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java
@@ -24,7 +24,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
-import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@@ -32,7 +31,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmCommentDao;
import org.thingsboard.server.dao.model.sql.AlarmCommentEntity;
-import org.thingsboard.server.dao.sql.JpaAbstractDao;
+import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.util.SqlDao;
@@ -45,7 +44,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_TABL
@Component
@SqlDao
@RequiredArgsConstructor
-public class JpaAlarmCommentDao extends JpaAbstractDao implements AlarmCommentDao {
+public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao implements AlarmCommentDao {
private final SqlPartitioningRepository partitioningRepository;
@Value("${sql.alarm_comments.partition_size:168}")
private int partitionSizeInHours;
@@ -54,21 +53,7 @@ public class JpaAlarmCommentDao extends JpaAbstractDao findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink){
+ public PageData findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink) {
log.trace("Try to find alarm comments by alarm id using [{}]", id);
return DaoUtil.toPageData(
alarmCommentRepository.findAllByAlarmId(id.getId(), DaoUtil.toPageable(pageLink)));
@@ -86,6 +71,11 @@ public class JpaAlarmCommentDao extends JpaAbstractDao getEntityClass() {
return AlarmCommentEntity.class;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
index 7fafbc177d..622a3298b9 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
@@ -59,5 +59,11 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIds(@Param("entityType") String entityType, @Param("entityIds") List entityIds);
+
+ @Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE entity_type = :entityType " +
+ "AND entity_id in :entityIds AND attribute_type = :attributeType ORDER BY attribute_key", nativeQuery = true)
+ List findAllKeysByEntityIdsAndAttributeType(@Param("entityType") String entityType,
+ @Param("entityIds") List entityIds,
+ @Param("attributeType") String attributeType);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
index fb300f17ab..cb51a7d8ad 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
@@ -153,6 +153,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
.findAllKeysByEntityIds(entityType.name(), entityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
}
+ @Override
+ public List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, EntityType entityType, List entityIds, String attributeType) {
+ return attributeKvRepository
+ .findAllKeysByEntityIdsAndAttributeType(entityType.name(), entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), attributeType);
+ }
+
@Override
public ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) {
AttributeKvEntity entity = new AttributeKvEntity();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
index f78dc339dc..e859b1538e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
@@ -15,8 +15,6 @@
*/
package org.thingsboard.server.dao.sql.audit;
-import com.datastax.oss.driver.api.core.uuid.Uuids;
-import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -25,10 +23,8 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
-import org.thingsboard.server.common.data.id.AuditLogId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
@@ -36,12 +32,11 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sql.AuditLogEntity;
-import org.thingsboard.server.dao.sql.JpaAbstractDao;
+import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
-import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -49,7 +44,7 @@ import java.util.concurrent.TimeUnit;
@SqlDao
@RequiredArgsConstructor
@Slf4j
-public class JpaAuditLogDao extends JpaAbstractDao implements AuditLogDao {
+public class JpaAuditLogDao extends JpaPartitionedAbstractDao implements AuditLogDao {
private final AuditLogRepository auditLogRepository;
private final SqlPartitioningRepository partitioningRepository;
@@ -72,25 +67,6 @@ public class JpaAuditLogDao extends JpaAbstractDao imp
return auditLogRepository;
}
- @Override
- public ListenableFuture saveByTenantId(AuditLog auditLog) {
- return service.submit(() -> {
- save(auditLog.getTenantId(), auditLog);
- return null;
- });
- }
-
- @Override
- public AuditLog save(TenantId tenantId, AuditLog auditLog) {
- if (auditLog.getId() == null) {
- UUID uuid = Uuids.timeBased();
- auditLog.setId(new AuditLogId(uuid));
- auditLog.setCreatedTime(Uuids.unixTimestamp(uuid));
- }
- partitioningRepository.createPartitionIfNotExists(TABLE_NAME, auditLog.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
- return super.save(tenantId, auditLog);
- }
-
@Override
public PageData findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) {
return DaoUtil.toPageData(
@@ -182,4 +158,9 @@ public class JpaAuditLogDao extends JpaAbstractDao imp
jdbcTemplate.update("CALL migrate_audit_logs(?, ?, ?)", startTime, endTime, partitionSizeInMs);
}
+ @Override
+ public void createPartition(AuditLogEntity entity) {
+ partitioningRepository.createPartitionIfNotExists(TABLE_NAME, entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
index 16e99f98b2..dc2759acd1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
@@ -77,7 +77,8 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
.setParameter("name", entity.getName())
.setParameter("scope", entity.getScope().name())
.setParameter("type", entity.getType().name())
- .setParameter("clustering_mode", entity.getClusteringMode().name());
+ .setParameter("clustering_mode", entity.getClusteringMode().name())
+ .setParameter("has_queue_name", entity.isHasQueueName());
}
private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java
index 2d4d1089ea..0352cdcda3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java
@@ -44,10 +44,10 @@ public class SqlComponentDescriptorInsertRepository extends AbstractComponentDes
}
private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) {
- return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, configuration_version, name, scope, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :configuration_version, :name, :scope, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *";
+ return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, configuration_version, name, scope, type, clustering_mode, has_queue_name) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :configuration_version, :name, :scope, :type, :clustering_mode, :has_queue_name) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *";
}
private static String getUpdateStatement(String id) {
- return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, configuration_version = :configuration_version, name = :name, scope = :scope, type = :type, clustering_mode = :clustering_mode";
+ return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, configuration_version = :configuration_version, name = :name, scope = :scope, type = :type, clustering_mode = :clustering_mode, has_queue_name = :has_queue_name";
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
index f5ecaaab84..3e8e8ebf54 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
@@ -35,7 +35,7 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.edge.EdgeEventDao;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sql.EdgeEventEntity;
-import org.thingsboard.server.dao.sql.JpaAbstractDao;
+import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
@@ -47,7 +47,6 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
-import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@@ -58,7 +57,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@SqlDao
@RequiredArgsConstructor
@Slf4j
-public class JpaBaseEdgeEventDao extends JpaAbstractDao implements EdgeEventDao {
+public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao implements EdgeEventDao {
private final UUID systemTenantId = NULL_UUID;
@@ -151,8 +150,9 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao save(EdgeEventEntity entity) {
@@ -227,4 +227,10 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao implements NotificationDao {
+public class JpaNotificationDao extends JpaPartitionedAbstractDao implements NotificationDao {
private final NotificationRepository notificationRepository;
private final SqlPartitioningRepository partitioningRepository;
@@ -52,18 +50,6 @@ public class JpaNotificationDao extends JpaAbstractDao findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ,
@@ -114,6 +100,12 @@ public class JpaNotificationDao extends JpaAbstractDao getEntityClass() {
return NotificationEntity.class;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java
index 9236d0fbb0..10833b11df 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId;
@@ -58,6 +59,7 @@ public class JpaOtaPackageInfoDao extends JpaAbstractDao findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
- if (query.getAggregation() == Aggregation.NONE) {
+ var aggParams = query.getAggParameters();
+ if (Aggregation.NONE.equals(aggParams.getAggregation())) {
return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
} else {
List>> futures = new ArrayList<>();
+ var intervalType = aggParams.getIntervalType();
long startPeriod = query.getStartTs();
long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
- long step = query.getInterval();
while (startPeriod < endPeriod) {
long startTs = startPeriod;
- long endTs = Math.min(startPeriod + step, endPeriod);
+ long endTs;
+ if (IntervalType.MILLISECONDS.equals(intervalType)) {
+ endTs = startPeriod + aggParams.getInterval();
+ } else {
+ endTs = TimeUtils.calculateIntervalEnd(startTs, intervalType, aggParams.getTzId());
+ }
+ endTs = Math.min(endTs, endPeriod);
long ts = startTs + (endTs - startTs) / 2;
ListenableFuture> aggregateTsKvEntry = findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation());
futures.add(aggregateTsKvEntry);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
index 6f1d569730..bb0ce06026 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
+import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@@ -35,11 +37,13 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
+import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
import org.thingsboard.server.dao.sqlts.insert.InsertTsRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesDao;
+import org.thingsboard.server.dao.util.TimeUtils;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import javax.annotation.PostConstruct;
@@ -143,14 +147,28 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
@Override
public ListenableFuture findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
+ var aggParams = query.getAggParameters();
+ var intervalType = aggParams.getIntervalType();
if (query.getAggregation() == Aggregation.NONE) {
return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
- } else {
+ } else if (IntervalType.MILLISECONDS.equals(intervalType)) {
long startTs = query.getStartTs();
long endTs = Math.max(query.getStartTs() + 1, query.getEndTs());
long timeBucket = query.getInterval();
List> data = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation());
return getReadTsKvQueryResultFuture(query, Futures.immediateFuture(data));
+ } else {
+ //TODO: @dshvaika improve according to native capabilities of Timescale.
+ long startPeriod = query.getStartTs();
+ long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
+ List timescaleTsKvEntities = new ArrayList<>();
+ while (startPeriod < endPeriod) {
+ long startTs = startPeriod;
+ long endTs = Math.min(TimeUtils.calculateIntervalEnd(startTs, intervalType, aggParams.getTzId()), endPeriod);
+ timescaleTsKvEntities.addAll(switchAggregation(query.getKey(), startTs, endTs, endTs - startTs, query.getAggregation(), entityId.getId()));
+ startPeriod = endTs;
+ }
+ return getReadTsKvQueryResultFuture(query, Futures.immediateFuture(toResultList(entityId, query.getKey(), timescaleTsKvEntities)));
}
}
@@ -187,6 +205,10 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
timescaleTsKvEntities.addAll(switchAggregation(key, startTs + interval, endTs, remainingPart, aggregation, entityId.getId()));
}
+ return toResultList(entityId, key, timescaleTsKvEntities);
+ }
+
+ private static List> toResultList(EntityId entityId, String key, List timescaleTsKvEntities) {
if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) {
List> result = new ArrayList<>();
timescaleTsKvEntities.forEach(entity -> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java
index 4477354f8d..31a419a5d0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java
@@ -95,7 +95,7 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
- if (query.getAggregation() == Aggregation.NONE) {
+ var aggParams = query.getAggParameters();
+ if (Aggregation.NONE.equals(aggParams.getAggregation())) {
return findAllAsyncWithLimit(tenantId, entityId, query);
} else {
long startPeriod = query.getStartTs();
long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
- long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS);
List>> futures = new ArrayList<>();
+ var intervalType = aggParams.getIntervalType();
while (startPeriod < endPeriod) {
long startTs = startPeriod;
- long endTs = Math.min(startPeriod + step, endPeriod);
- long ts = endTs - startTs;
- ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder());
+ long endTs;
+ if (IntervalType.MILLISECONDS.equals(intervalType)) {
+ endTs = startPeriod + Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS);
+ } else {
+ endTs = TimeUtils.calculateIntervalEnd(startTs, aggParams.getIntervalType(), aggParams.getTzId());
+ }
+ endTs = Math.min(endTs, endPeriod);
+ ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, query.getAggregation(), query.getOrder());
futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
startPeriod = endTs;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java
index 9c302be53d..8ab9a2c4da 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java
@@ -33,7 +33,7 @@ public class TsKvQueryCursor extends QueryCursor {
@Getter
private final List data;
@Getter
- private String orderBy;
+ private final String orderBy;
private int partitionIndex;
private int currentLimit;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
index 8c11b65d4b..1796540853 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -145,7 +145,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
.entity(savedUser)
.oldEntity(oldUser)
.entityId(savedUser.getId())
- .added(user.getId() == null).build());
+ .created(user.getId() == null).build());
return savedUser;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java
new file mode 100644
index 0000000000..3a768673b8
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/TimeUtils.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.util;
+
+import org.thingsboard.server.common.data.kv.IntervalType;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.IsoFields;
+import java.time.temporal.WeekFields;
+
+public class TimeUtils {
+
+ public static long calculateIntervalEnd(long startTs, IntervalType intervalType, ZoneId tzId) {
+ var startTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(startTs), tzId);
+ switch (intervalType) {
+ case WEEK:
+ return startTime.truncatedTo(ChronoUnit.DAYS).with(WeekFields.SUNDAY_START.dayOfWeek(), 1).plusDays(7).toInstant().toEpochMilli();
+ case WEEK_ISO:
+ return startTime.truncatedTo(ChronoUnit.DAYS).with(WeekFields.ISO.dayOfWeek(), 1).plusDays(7).toInstant().toEpochMilli();
+ case MONTH:
+ return startTime.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1).plusMonths(1).toInstant().toEpochMilli();
+ case QUARTER:
+ return startTime.truncatedTo(ChronoUnit.DAYS).with(IsoFields.DAY_OF_QUARTER, 1).plusMonths(3).toInstant().toEpochMilli();
+ default:
+ throw new RuntimeException("Not supported!");
+ }
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
index 44797d943d..7f7d4d5ae8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
@@ -97,7 +97,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
imageService.replaceBase64WithImageUrl(widgetTypeDetails);
WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId())
- .entityId(result.getId()).added(widgetTypeDetails.getId() == null).build());
+ .entityId(result.getId()).created(widgetTypeDetails.getId() == null).build());
return result;
} catch (Exception t) {
AbstractCachedEntityService.checkConstraintViolation(t,
@@ -209,7 +209,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
widgetTypeDao.saveWidgetsBundleWidget(widgetsBundleWidget);
}
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
- .entityId(widgetsBundleId).added(false).build());
+ .entityId(widgetsBundleId).created(false).build());
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
index ae3556cff9..9d6960a7d1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
@@ -78,7 +78,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
imageService.replaceBase64WithImageUrl(widgetsBundle, "bundle");
WidgetsBundle result = widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId())
- .entityId(result.getId()).added(widgetsBundle.getId() == null).build());
+ .entityId(result.getId()).created(widgetsBundle.getId() == null).build());
return result;
} catch (Exception e) {
AbstractCachedEntityService.checkConstraintViolation(e,
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 9dc6c6a6b1..2352ea2eb3 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -126,7 +126,8 @@ CREATE TABLE IF NOT EXISTS component_descriptor (
name varchar(255),
scope varchar(255),
type varchar(255),
- clustering_mode varchar(255)
+ clustering_mode varchar(255),
+ has_queue_name boolean DEFAULT false
);
CREATE TABLE IF NOT EXISTS customer (
@@ -187,6 +188,7 @@ CREATE TABLE IF NOT EXISTS rule_node (
name varchar(255),
debug_mode boolean,
singleton_mode boolean,
+ queue_name varchar(255),
external_id uuid
);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java
index f632273cfe..1976f173a5 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java
@@ -23,8 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
-import org.springframework.transaction.support.TransactionCallbackWithoutResult;
-import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.StringUtils;
@@ -32,21 +30,22 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
-import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.asset.AssetDao;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.RelationService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@@ -60,6 +59,8 @@ public class AssetServiceTest extends AbstractServiceTest {
@Autowired
CustomerService customerService;
@Autowired
+ RelationService relationService;
+ @Autowired
private AssetProfileService assetProfileService;
@Autowired
private PlatformTransactionManager platformTransactionManager;
@@ -252,11 +253,15 @@ public class AssetServiceTest extends AbstractServiceTest {
asset.setName("My asset");
asset.setType("default");
Asset savedAsset = assetService.saveAsset(asset);
+ EntityRelation relation = new EntityRelation(tenantId, savedAsset.getId(), EntityRelation.CONTAINS_TYPE);
+ relationService.saveRelation(tenantId, relation);
+
Asset foundAsset = assetService.findAssetById(tenantId, savedAsset.getId());
Assert.assertNotNull(foundAsset);
assetService.deleteAsset(tenantId, savedAsset.getId());
foundAsset = assetService.findAssetById(tenantId, savedAsset.getId());
Assert.assertNull(foundAsset);
+ Assert.assertTrue(relationService.findByTo(tenantId, savedAsset.getId(), RelationTypeGroup.COMMON).isEmpty());
}
@Test
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java
index aa407e2a8e..f6ef6e8940 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java
@@ -85,6 +85,6 @@ public class JpaAlarmCommentDaoTest extends AbstractJpaDaoTest {
alarmComment.setUserId(new UserId(userId));
alarmComment.setType(type);
alarmComment.setComment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10)));
- alarmCommentDao.createAlarmComment(TenantId.fromUUID(UUID.randomUUID()), alarmComment);
+ alarmCommentDao.save(TenantId.fromUUID(UUID.randomUUID()), alarmComment);
}
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/TimeUtilsTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/TimeUtilsTest.java
new file mode 100644
index 0000000000..a3df974236
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/util/TimeUtilsTest.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.util;
+
+import org.junit.jupiter.api.Test;
+import org.thingsboard.server.common.data.kv.IntervalType;
+
+import java.time.ZoneId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TimeUtilsTest {
+
+ @Test
+ void testWeekEnd() {
+ long ts = 1704899727000L; // Wednesday, January 10 15:15:27 GMT
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.WEEK, ZoneId.of("Europe/Kyiv"))).isEqualTo(1705183200000L); // Sunday, January 14, 2024 0:00:00 GMT+02:00
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.WEEK_ISO, ZoneId.of("Europe/Kyiv"))).isEqualTo(1705269600000L); // Monday, January 15, 2024 0:00:00 GMT+02:00
+
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.WEEK, ZoneId.of("Europe/Amsterdam"))).isEqualTo(1705186800000L); // Sunday, January 14, 2024 0:00:00 GMT+01:00
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.WEEK_ISO, ZoneId.of("Europe/Amsterdam"))).isEqualTo(1705273200000L); // Monday, January 15, 2024 0:00:00 GMT+01:00
+
+ ts = 1704621600000L; // Sunday, January 7, 2024 12:00:00 GMT+02:00
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.WEEK, ZoneId.of("Europe/Kyiv"))).isEqualTo(1705183200000L); // Sunday, January 14, 2024 0:00:00 GMT+02:00
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.WEEK_ISO, ZoneId.of("Europe/Kyiv"))).isEqualTo(1704664800000L); // Monday, January 8, 2024 0:00:00 GMT+02:00
+ }
+
+
+ @Test
+ void testMonthEnd() {
+ long ts = 1704899727000L; // Wednesday, January 10 15:15:27 GMT
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.MONTH, ZoneId.of("Europe/Kyiv"))).isEqualTo(1706738400000L); // Thursday, February 1, 2024 0:00:00 GMT+02:00
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.MONTH, ZoneId.of("Europe/Amsterdam"))).isEqualTo(1706742000000L); // Monday, January 15, 2024 0:00:00 GMT+02:00
+ }
+
+ @Test
+ void testQuarterEnd() {
+ long ts = 1704899727000L; // Wednesday, January 10 15:15:27 GMT
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.QUARTER, ZoneId.of("Europe/Kyiv"))).isEqualTo(1711918800000L); // Monday, April 1, 2024 0:00:00 GMT+03:00 DST
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.QUARTER, ZoneId.of("Europe/Amsterdam"))).isEqualTo(1711922400000L); // Monday, April 1, 2024 1:00:00 GMT+03:00 DST
+
+ ts = 1711929600000L; // Monday, April 1, 2024 3:00:00 GMT+03:00
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.QUARTER, ZoneId.of("Europe/Kyiv"))).isEqualTo(1719781200000L); // Monday, July 1, 2024 0:00:00 GMT+03:00 DST
+ assertThat(TimeUtils.calculateIntervalEnd(ts, IntervalType.QUARTER, ZoneId.of("America/New_York"))).isEqualTo(1711944000000L); // Monday, April 1, 2024 7:00:00 GMT+03:00 DST
+ }
+
+}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java
index 52ee02d8de..1d66c5787c 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java
@@ -23,6 +23,10 @@ public interface MonitoringTarget {
String getBaseUrl();
+ default String getQueue() {
+ return "Main";
+ }
+
boolean isCheckDomainIps();
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java
index 6f4211a870..0fda965483 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java
@@ -22,10 +22,15 @@ public class TransportInfo {
private final TransportType transportType;
private final String baseUrl;
+ private final String queue;
@Override
public String toString() {
- return String.format("%s transport (%s)", transportType, baseUrl);
+ if (queue.equals("Main")) {
+ return String.format("*%s* (%s)", transportType.getName(), baseUrl);
+ } else {
+ return String.format("*%s* (%s) _%s_", transportType.getName(), baseUrl, queue);
+ }
}
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java
index 3245f1b956..e2b3258945 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java
@@ -16,6 +16,7 @@
package org.thingsboard.monitoring.config.transport;
import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
import org.thingsboard.monitoring.config.MonitoringTarget;
import java.util.UUID;
@@ -25,6 +26,7 @@ public class TransportMonitoringTarget implements MonitoringTarget {
private String baseUrl;
private DeviceConfig device; // set manually during initialization
+ private String queue;
private boolean checkDomainIps;
@Override
@@ -32,4 +34,8 @@ public class TransportMonitoringTarget implements MonitoringTarget {
return device.getId();
}
+ public String getQueue() {
+ return StringUtils.defaultIfEmpty(queue, "Main");
+ }
+
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java
index f2241b5374..4a742c5dd0 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java
@@ -27,11 +27,12 @@ import org.thingsboard.monitoring.service.transport.impl.MqttTransportHealthChec
@Getter
public enum TransportType {
- MQTT(MqttTransportHealthChecker.class),
- COAP(CoapTransportHealthChecker.class),
- HTTP(HttpTransportHealthChecker.class),
- LWM2M(Lwm2mTransportHealthChecker.class);
+ MQTT("MQTT", MqttTransportHealthChecker.class),
+ COAP("CoAP",CoapTransportHealthChecker.class),
+ HTTP("HTTP", HttpTransportHealthChecker.class),
+ LWM2M("LwM2M", Lwm2mTransportHealthChecker.class);
+ private final String name;
private final Class extends TransportHealthChecker>> serviceClass;
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java
index 971c6ecf8e..f77098c3c3 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/HighLatencyNotification.java
@@ -34,7 +34,7 @@ public class HighLatencyNotification implements Notification {
StringBuilder text = new StringBuilder();
text.append("Some of the latencies are higher than ").append(thresholdMs).append(" ms:\n");
highLatencies.forEach(latency -> {
- text.append(String.format("[%s] %s\n", latency.getKey(), latency.getFormattedValue()));
+ text.append(String.format("[%s] *%s*\n", latency.getKey(), latency.getFormattedValue()));
});
return text.toString();
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java
index af6e597dc9..63af34a0fb 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceFailureNotification.java
@@ -43,7 +43,7 @@ public class ServiceFailureNotification implements Notification {
if (errorMsg == null) {
errorMsg = error.getClass().getSimpleName();
}
- return String.format("[%s] Failure: %s (number of subsequent failures: %s)", serviceKey, errorMsg, failuresCount);
+ return String.format("%s - Failure: %s (number of subsequent failures: %s)", serviceKey, errorMsg, failuresCount);
}
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java
index b1735e6645..e6c1e85b83 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/ServiceRecoveryNotification.java
@@ -25,7 +25,7 @@ public class ServiceRecoveryNotification implements Notification {
@Override
public String getText() {
- return String.format("[%s] is OK", serviceKey);
+ return String.format("%s is OK", serviceKey);
}
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java
index 1086165a40..1949584d7f 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java
@@ -17,6 +17,8 @@ package org.thingsboard.monitoring.notification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.monitoring.data.notification.Notification;
import org.thingsboard.monitoring.notification.channels.NotificationChannel;
@@ -24,7 +26,6 @@ import org.thingsboard.monitoring.notification.channels.NotificationChannel;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.function.Consumer;
@Component
@RequiredArgsConstructor
@@ -34,15 +35,20 @@ public class NotificationService {
private final List notificationChannels;
private final ExecutorService notificationExecutor = Executors.newSingleThreadExecutor();
- public void sendNotification(Notification notification) {
- forEachNotificationChannel(notificationChannel -> notificationChannel.sendNotification(notification));
- }
+ @Value("${monitoring.notifications.message_prefix}")
+ private String messagePrefix;
- private void forEachNotificationChannel(Consumer function) {
+ public void sendNotification(Notification notification) {
+ String message;
+ if (StringUtils.isEmpty(messagePrefix)) {
+ message = notification.getText();
+ } else {
+ message = messagePrefix + System.lineSeparator() + notification.getText();
+ }
notificationChannels.forEach(notificationChannel -> {
notificationExecutor.submit(() -> {
try {
- function.accept(notificationChannel);
+ notificationChannel.sendNotification(message);
} catch (Exception e) {
log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e);
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java
index 6841756607..9ee52608ba 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/NotificationChannel.java
@@ -15,10 +15,8 @@
*/
package org.thingsboard.monitoring.notification.channels;
-import org.thingsboard.monitoring.data.notification.Notification;
-
public interface NotificationChannel {
- void sendNotification(Notification notification);
+ void sendNotification(String message);
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java
index 3a6a7a0efb..e508159b8b 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/channels/impl/SlackNotificationChannel.java
@@ -21,7 +21,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
-import org.thingsboard.monitoring.data.notification.Notification;
import org.thingsboard.monitoring.notification.channels.NotificationChannel;
import javax.annotation.PostConstruct;
@@ -29,11 +28,11 @@ import java.time.Duration;
import java.util.Map;
@Component
-@ConditionalOnProperty(value = "monitoring.notification_channels.slack.enabled", havingValue = "true")
+@ConditionalOnProperty(value = "monitoring.notifications.slack.enabled", havingValue = "true")
@Slf4j
public class SlackNotificationChannel implements NotificationChannel {
- @Value("${monitoring.notification_channels.slack.webhook_url}")
+ @Value("${monitoring.notifications.slack.webhook_url}")
private String webhookUrl;
private RestTemplate restTemplate;
@@ -47,11 +46,7 @@ public class SlackNotificationChannel implements NotificationChannel {
}
@Override
- public void sendNotification(Notification notification) {
- sendNotification(notification.getText());
- }
-
- private void sendNotification(String message) {
+ public void sendNotification(String message) {
restTemplate.postForObject(webhookUrl, Map.of("text", message), String.class);
}
diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java
index 8dbd39187c..bee65d1f37 100644
--- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java
+++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java
@@ -29,6 +29,8 @@ import org.thingsboard.monitoring.service.BaseHealthChecker;
import org.thingsboard.monitoring.util.ResourceUtils;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileType;
+import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
@@ -38,6 +40,9 @@ import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
+import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
+import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
+import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
@@ -45,27 +50,19 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType;
@Slf4j
public abstract class TransportHealthChecker extends BaseHealthChecker {
- private static final String DEFAULT_DEVICE_NAME = "[Monitoring] %s transport (%s)";
- private static final String DEFAULT_PROFILE_NAME = "[Monitoring] %s";
-
public TransportHealthChecker(C config, TransportMonitoringTarget target) {
super(config, target);
}
@Override
protected void initialize(TbClient tbClient) {
- String deviceName = String.format(DEFAULT_DEVICE_NAME, config.getTransportType(), target.getBaseUrl());
- Device device = tbClient.getTenantDevice(deviceName)
- .orElseGet(() -> {
- log.info("Creating new device '{}'", deviceName);
- return createDevice(config.getTransportType(), deviceName, tbClient);
- });
+ Device device = getOrCreateDevice(tbClient);
DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(device.getId())
.orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + device.getId()));
DeviceConfig deviceConfig = new DeviceConfig();
deviceConfig.setId(device.getId().toString());
- deviceConfig.setName(deviceName);
+ deviceConfig.setName(device.getName());
deviceConfig.setCredentials(credentials);
target.setDevice(deviceConfig);
}
@@ -77,51 +74,43 @@ public abstract class TransportHealthChecker {
- TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class);
- log.info("Creating LwM2M resource");
- return tbClient.saveResource(newResource);
- });
- String profileName = String.format(DEFAULT_PROFILE_NAME, transportType);
- DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData()
- .stream().findFirst()
- .orElseGet(() -> {
- DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class);
- newProfile.setName(profileName);
- log.info("Creating LwM2M device profile");
- return tbClient.saveDeviceProfile(newProfile);
- });
- device.setType(profileName);
- device.setDeviceProfileId(profile.getId());
deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration());
-
credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials();
NoSecClientCredential client = new NoSecClientCredential();
@@ -133,7 +122,42 @@ public abstract class TransportHealthChecker {
+ TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class);
+ log.info("Creating LwM2M resource");
+ return tbClient.saveResource(newResource);
+ });
+ deviceProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class);
+ }
+
+ deviceProfile.setName(profileName);
+ deviceProfile.setDefaultQueueName(target.getQueue());
+ return tbClient.saveDeviceProfile(deviceProfile);
+ }
+
}
diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml
index 32af8636cd..1f65f48ca9 100644
--- a/monitoring/src/main/resources/tb-monitoring.yml
+++ b/monitoring/src/main/resources/tb-monitoring.yml
@@ -38,7 +38,7 @@ monitoring:
check_timeout_ms: '${CHECK_TIMEOUT_MS:5000}'
# Failures threshold for notifying
- failures_threshold: '${FAILURES_THRESHOLD:2}'
+ failures_threshold: '${FAILURES_THRESHOLD:1}'
# Notify after each REPEATED_FAILURE_NOTIFICATION subsequent failures, 0 to notify only once on first failure
repeated_failure_notification: '${REPEATED_FAILURE_NOTIFICATION:4}'
@@ -53,6 +53,8 @@ monitoring:
targets:
# MQTT transport base url, tcp://DOMAIN:1883 by default
- base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}'
+ # Queue to use for target device
+ queue: '${MQTT_TRANSPORT_USED_QUEUE:Main}'
# Whether to monitor IPs associated with the domain from base url
check_domain_ips: '${MQTT_TRANSPORT_CHECK_DOMAIN_IPS:false}'
# To add more targets, use following environment variables:
@@ -66,6 +68,8 @@ monitoring:
targets:
# CoAP transport base url, coap://DOMAIN by default
- base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}'
+ # Queue to use for target device
+ queue: '${COAP_TRANSPORT_USED_QUEUE:Main}'
# Whether to monitor IPs associated with the domain from base url
check_domain_ips: '${COAP_TRANSPORT_CHECK_DOMAIN_IPS:false}'
# To add more targets, use following environment variables:
@@ -79,6 +83,8 @@ monitoring:
targets:
# HTTP transport base url, http://DOMAIN by default
- base_url: '${HTTP_TRANSPORT_BASE_URL:http://${monitoring.domain}}'
+ # Queue to use for target device
+ queue: '${HTTP_TRANSPORT_USED_QUEUE:Main}'
# Whether to monitor IPs associated with the domain from base url
check_domain_ips: '${HTTP_TRANSPORT_CHECK_DOMAIN_IPS:false}'
# To add more targets, use following environment variables:
@@ -92,12 +98,15 @@ monitoring:
targets:
# LwM2M transport base url, coap://DOMAIN:5685 by default
- base_url: '${LWM2M_TRANSPORT_BASE_URL:coap://${monitoring.domain}:5685}'
+ # Queue to use for target device
+ queue: '${LWM2M_TRANSPORT_USED_QUEUE:Main}'
# Whether to monitor IPs associated with the domain from base url
check_domain_ips: '${LWM2M_TRANSPORT_CHECK_DOMAIN_IPS:false}'
# To add more targets, use following environment variables:
# monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[2].base_url, etc.
- notification_channels:
+ notifications:
+ message_prefix: '${NOTIFICATION_MESSAGE_PREFIX:}'
slack:
# Enable notifying via Slack
enabled: '${SLACK_NOTIFICATION_CHANNEL_ENABLED:false}'
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
index b9f8c3fdb3..15c3ccbbba 100644
--- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
@@ -107,6 +107,7 @@ public class ContainerTestSuite {
List composeFiles = new ArrayList<>(Arrays.asList(
new File(targetDir + "docker-compose.yml"),
new File(targetDir + "docker-compose.volumes.yml"),
+ new File(targetDir + "docker-compose.mosquitto.yml"),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid-test-extras.yml" : "docker-compose.postgres-test-extras.yml")),
new File(targetDir + "docker-compose.postgres.volumes.yml"),
@@ -162,6 +163,7 @@ public class ContainerTestSuite {
.withEnv(queueEnv)
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
+ .withExposedService("broker", 1883)
.waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
index 25e3acd16c..d33c740603 100644
--- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
@@ -48,4 +48,13 @@ public class TestProperties {
}
return System.getProperty("tb.wsUrl", "ws://localhost:8080");
}
+
+ public static String getMqttBrokerUrl() {
+ if (instance.isActive()) {
+ String host = instance.getTestContainer().getServiceHost("broker", 1883);
+ Integer port = instance.getTestContainer().getServicePort("broker", 1883);
+ return "tcp://" + host + ":" + port;
+ }
+ return System.getProperty("mqtt.broker", "tcp://localhost:1883");
+ }
}
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
index 25390e6306..305e8706f2 100644
--- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
@@ -31,10 +31,12 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
+import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
@@ -45,9 +47,11 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
@@ -527,4 +531,41 @@ public class TestRestClient {
.then()
.statusCode(HTTP_OK);
}
+
+ public PageData getEvents(EntityId entityId, EventType eventType, TenantId tenantId, TimePageLink pageLink) {
+ Map params = new HashMap<>();
+ params.put("entityType", entityId.getEntityType().name());
+ params.put("entityId", entityId.getId().toString());
+ params.put("eventType", eventType.name());
+ params.put("tenantId", tenantId.getId().toString());
+ addTimePageLinkToParam(params, pageLink);
+
+ return given().spec(requestSpec)
+ .get("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&" + getTimeUrlParams(pageLink), params)
+ .then()
+ .statusCode(HTTP_OK)
+ .extract()
+ .as(new TypeRef<>() {});
+ }
+
+ private void addTimePageLinkToParam(Map params, TimePageLink pageLink) {
+ this.addPageLinkToParam(params, pageLink);
+ if (pageLink.getStartTime() != null) {
+ params.put("startTime", String.valueOf(pageLink.getStartTime()));
+ }
+ if (pageLink.getEndTime() != null) {
+ params.put("endTime", String.valueOf(pageLink.getEndTime()));
+ }
+ }
+
+ private String getTimeUrlParams(TimePageLink pageLink) {
+ String urlParams = getUrlParams(pageLink);
+ if (pageLink.getStartTime() != null) {
+ urlParams += "&startTime={startTime}";
+ }
+ if (pageLink.getEndTime() != null) {
+ urlParams += "&endTime={endTime}";
+ }
+ return urlParams;
+ }
}
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/rule/node/MqttNodeTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/rule/node/MqttNodeTest.java
new file mode 100644
index 0000000000..07a0691924
--- /dev/null
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/rule/node/MqttNodeTest.java
@@ -0,0 +1,204 @@
+/**
+ * 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.msa.rule.node;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.awaitility.Awaitility;
+import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EventInfo;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.event.EventType;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.msa.AbstractContainerTest;
+import org.thingsboard.server.msa.DisableUIListeners;
+import org.thingsboard.server.msa.TestProperties;
+import org.thingsboard.server.msa.WsClient;
+import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.testng.Assert.fail;
+import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
+
+@DisableUIListeners
+@Slf4j
+public class MqttNodeTest extends AbstractContainerTest {
+
+ private static final String TOPIC = "tb/mqtt/device";
+
+ private Device device;
+
+ @BeforeMethod
+ public void setUp() {
+ testRestClient.login("tenant@thingsboard.org", "tenant");
+ device = testRestClient.postDevice("", defaultDevicePrototype("mqtt_"));
+ }
+
+ @AfterMethod
+ public void tearDown() {
+ testRestClient.deleteDeviceIfExists(device.getId());
+ }
+
+ @Test
+ public void telemetryUpload() throws Exception {
+ RuleChainId defaultRuleChainId = getDefaultRuleChainId();
+
+ createRootRuleChainWithTestNode("MqttRuleNodeTestMetadata.json", "org.thingsboard.rule.engine.mqtt.TbMqttNode", 2);
+
+ DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
+
+ WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
+
+ MqttMessageListener messageListener = new MqttMessageListener();
+ MqttClient responseClient = new MqttClient(TestProperties.getMqttBrokerUrl(), StringUtils.randomAlphanumeric(10), new MemoryPersistence());
+ responseClient.connect();
+ responseClient.subscribe(TOPIC, messageListener);
+
+ MqttClient mqttClient = new MqttClient("tcp://localhost:1883", StringUtils.randomAlphanumeric(10), new MemoryPersistence());
+ MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
+ mqttConnectOptions.setUserName(deviceCredentials.getCredentialsId());
+ mqttClient.connect(mqttConnectOptions);
+ mqttClient.publish("v1/devices/me/telemetry", new MqttMessage(createPayload().toString().getBytes()));
+
+ WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
+ log.info("Received telemetry: {}", actualLatestTelemetry);
+ wsClient.closeBlocking();
+
+ assertThat(actualLatestTelemetry.getData()).hasSize(4);
+ assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
+
+ assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
+ assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
+ assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
+ assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
+
+ Awaitility
+ .await()
+ .alias("Get integration events")
+ .atMost(10, TimeUnit.SECONDS)
+ .until(() -> messageListener.getEvents().size() > 0);
+
+ BlockingQueue events = messageListener.getEvents();
+ JsonNode actual = JacksonUtil.toJsonNode(Objects.requireNonNull(events.poll()).message);
+
+ assertThat(actual.get("stringKey").asText()).isEqualTo("value1");
+ assertThat(actual.get("booleanKey").asBoolean()).isEqualTo(Boolean.TRUE);
+ assertThat(actual.get("doubleKey").asDouble()).isEqualTo(42.0);
+ assertThat(actual.get("longKey").asLong()).isEqualTo(73);
+
+ testRestClient.setRootRuleChain(defaultRuleChainId);
+ }
+
+ @Data
+ private class MqttMessageListener implements IMqttMessageListener {
+ private final BlockingQueue events;
+
+ private MqttMessageListener() {
+ events = new ArrayBlockingQueue<>(100);
+ }
+
+ @Override
+ public void messageArrived(String s, MqttMessage mqttMessage) {
+ log.info("MQTT message [{}], topic [{}]", mqttMessage.toString(), s);
+ events.add(new MqttEvent(s, mqttMessage.toString()));
+ }
+
+ public BlockingQueue getEvents() {
+ return events;
+ }
+ }
+
+ @Data
+ private class MqttEvent {
+ private final String topic;
+ private final String message;
+ }
+
+ private RuleChainId getDefaultRuleChainId() {
+ PageData ruleChains = testRestClient.getRuleChains(new PageLink(40, 0));
+
+ Optional defaultRuleChain = ruleChains.getData()
+ .stream()
+ .filter(RuleChain::isRoot)
+ .findFirst();
+ if (!defaultRuleChain.isPresent()) {
+ fail("Root rule chain wasn't found");
+ }
+ return defaultRuleChain.get().getId();
+ }
+
+ protected RuleChainId createRootRuleChainWithTestNode(String ruleChainMetadataFile, String ruleNodeType, int eventsCount) throws Exception {
+ RuleChain newRuleChain = new RuleChain();
+ newRuleChain.setName("testRuleChain");
+ RuleChain ruleChain = testRestClient.postRuleChain(newRuleChain);
+
+ JsonNode configuration = JacksonUtil.OBJECT_MAPPER.readTree(this.getClass().getClassLoader().getResourceAsStream(ruleChainMetadataFile));
+ RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
+ ruleChainMetaData.setRuleChainId(ruleChain.getId());
+ ruleChainMetaData.setFirstNodeIndex(configuration.get("firstNodeIndex").asInt());
+ ruleChainMetaData.setNodes(Arrays.asList(JacksonUtil.OBJECT_MAPPER.treeToValue(configuration.get("nodes"), RuleNode[].class)));
+ ruleChainMetaData.setConnections(Arrays.asList(JacksonUtil.OBJECT_MAPPER.treeToValue(configuration.get("connections"), NodeConnectionInfo[].class)));
+
+ ruleChainMetaData = testRestClient.postRuleChainMetadata(ruleChainMetaData);
+
+ testRestClient.setRootRuleChain(ruleChain.getId());
+
+ RuleNode node = ruleChainMetaData.getNodes().stream().filter(ruleNode -> ruleNode.getType().equals(ruleNodeType)).findFirst().get();
+
+ Awaitility
+ .await()
+ .alias("Get events from rule chain")
+ .atMost(10, TimeUnit.SECONDS)
+ .until(() -> {
+ PageData