diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json
index 12716482c9..2abf10e995 100644
--- a/application/src/main/data/json/system/widget_types/alarms_table.json
+++ b/application/src/main/data/json/system/widget_types/alarms_table.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings",
diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json
index fe5722a01a..a74d2dd644 100644
--- a/application/src/main/data/json/system/widget_types/asset_admin_table.json
+++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",
diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json
index 35cb457590..3265554da1 100644
--- a/application/src/main/data/json/system/widget_types/device_admin_table.json
+++ b/application/src/main/data/json/system/widget_types/device_admin_table.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",
diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json
index 73a546cb87..ce3ec3add1 100644
--- a/application/src/main/data/json/system/widget_types/entities_table.json
+++ b/application/src/main/data/json/system/widget_types/entities_table.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",
diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json
index 4ccee86d5e..59e5672b53 100644
--- a/application/src/main/data/json/tenant/dashboards/gateways.json
+++ b/application/src/main/data/json/tenant/dashboards/gateways.json
@@ -1,8 +1,8 @@
{
"title": "ThingsBoard IoT Gateways",
- "image": null,
+ "image": "tb-image:Z2F0ZXdheS1kYXNoYm9hcmQucG5n:Z2F0ZXdheS1kYXNoYm9hcmQucG5n;data:image/png;base64,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",
"mobileHide": false,
- "mobileOrder": null,
+ "mobileOrder": 6,
"configuration": {
"description": "",
"widgets": {
@@ -136,7 +136,7 @@
{
"name": "active_connectors",
"type": "attribute",
- "label": "Active Connectors",
+ "label": "Enabled Connectors",
"color": "#3f51b5",
"settings": {
"columnWidth": "20%",
@@ -248,7 +248,7 @@
"type": "customPretty",
"customHtml": "
\r\n",
"customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n",
- "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n setTimeout(function() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").addEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").addEventListener('click', goToConfigState);\r\n }, 500);\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").removeEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").removeEventListener('click', goToConfigState);\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n",
+ "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n",
"customResources": [],
"openInSeparateDialog": false,
"openInPopover": false,
@@ -567,7 +567,8 @@
"padding": "8px",
"settings": {
"useMarkdownTextFunction": true,
- "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n \n \n ${generateMatHeader(index)}\n ${label}\n \n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Active | Inactive)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `${blockData}
`;",
+ "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.",
+ "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n \n \n ${generateMatHeader(index)}\n ${label}\n \n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Enabled | Disabled)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `${blockData}
`;",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}"
},
@@ -5218,7 +5219,7 @@
"widgets": {
"02a05113-d878-b086-328e-75bd6f02b2bb": {
"sizeX": 24,
- "sizeY": 11,
+ "sizeY": 12,
"row": 0,
"col": 0
}
@@ -5230,7 +5231,7 @@
"backgroundSizeMode": "100%",
"autoFillHeight": true,
"backgroundImageUrl": null,
- "mobileAutoFillHeight": false,
+ "mobileAutoFillHeight": true,
"mobileRowHeight": 70,
"outerMargin": true
}
@@ -6741,4 +6742,4 @@
},
"externalId": null,
"name": "Gateway"
-}
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
index 0c3146a6c6..38f44898ee 100644
--- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
@@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
-import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
@@ -68,6 +67,8 @@ import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import static org.eclipse.leshan.core.LwM2m.Version.V1_0;
+
@Service
@TbCoreComponent
@RequiredArgsConstructor
@@ -256,7 +257,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList());
- transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null));
+ transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()));
DeviceProfileData deviceProfileData = new DeviceProfileData();
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
index 0c91d72ea5..438cb58b91 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
+import org.springframework.core.env.Environment;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@@ -39,11 +40,15 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
@Slf4j
public abstract class AbstractTbEntityService {
+ @Autowired
+ private Environment env;
+
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@@ -67,6 +72,10 @@ public abstract class AbstractTbEntityService {
@Lazy
private EntitiesVersionControlService vcService;
+ protected boolean isTestProfile() {
+ return Set.of(this.env.getActiveProfiles()).contains("test");
+ }
+
protected T checkNotNull(T reference) throws ThingsboardException {
return checkNotNull(reference, "Requested item wasn't found!");
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
index 8096722940..74061ee9e5 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.entitiy.tenant;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
@@ -52,7 +53,9 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T
Tenant savedTenant = tenantService.saveTenant(tenant, tenantId -> {
installScripts.createDefaultRuleChains(tenantId);
installScripts.createDefaultEdgeRuleChains(tenantId);
- installScripts.createDefaultTenantDashboards(tenantId, null);
+ if (!isTestProfile()) {
+ installScripts.createDefaultTenantDashboards(tenantId, null);
+ }
});
tenantProfileCache.evict(savedTenant.getId());
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
index ae16f925a0..37a55c99d6 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
@@ -27,11 +27,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.housekeeper.HousekeeperConfig;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.service.queue.consumer.QueueConsumerManager;
import javax.annotation.PreDestroy;
import java.util.LinkedHashSet;
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
index 06e38d0160..eea92eb75b 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
@@ -27,13 +27,13 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.housekeeper.HousekeeperConfig;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor;
import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService;
-import org.thingsboard.server.service.queue.consumer.QueueConsumerManager;
import javax.annotation.PreDestroy;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
index 999079e913..9db2a99e7f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -317,7 +317,8 @@ public class InstallScripts {
@SneakyThrows
public void loadSystemImages() {
log.info("Loading system images...");
- Stream dashboardsFiles = Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR));
+ Stream dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
+ Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
try (dashboardsFiles) {
dashboardsFiles.forEach(file -> {
try {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
index 06cbdc2230..9626190f1b 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
@@ -39,6 +39,7 @@ import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
@@ -77,6 +78,8 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private CustomerService customerService;
+ @Autowired
+ private TenantProfileService tenantProfileService;
@Override
public void updateData(String fromVersion) throws Exception {
@@ -88,12 +91,31 @@ public class DefaultDataUpdateService implements DataUpdateService {
case "3.6.4":
log.info("Updating data from version 3.6.4 to 3.7.0 ...");
updateCustomersWithTheSameTitle();
+ updateMaxRuleNodeExecsPerMessage();
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
}
+ private void updateMaxRuleNodeExecsPerMessage() {
+ var tenantProfiles = new PageDataIterable<>(
+ link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE);
+ tenantProfiles.forEach(tenantProfile -> {
+ var configurationOpt = tenantProfile.getProfileConfiguration();
+ configurationOpt.ifPresent(configuration -> {
+ if (configuration.getMaxRuleNodeExecsPerMessage() == 0) {
+ configuration.setMaxRuleNodeExecutionsPerMessage(1000);
+ try {
+ tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
+ } catch (Exception e) {
+ log.error("Failed to update tenant profile with id: {} due to: ", tenantProfile.getId(), e);
+ }
+ }
+ });
+ });
+ }
+
private void updateCustomersWithTheSameTitle() {
var customers = new ArrayList();
new PageDataIterable<>(pageLink ->
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 29db1d7d76..257dfa14b1 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
@@ -16,12 +16,13 @@
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.Futures;
import freemarker.template.Configuration;
import freemarker.template.Template;
-import jakarta.xml.bind.DatatypeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.NestedRuntimeException;
@@ -31,29 +32,36 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.TbEmail;
+import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageRecordState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import jakarta.mail.internet.MimeMessage;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -76,13 +84,21 @@ public class DefaultMailService implements MailService {
private TbApiUsageStateService apiUsageStateService;
@Autowired
- private MailExecutorService mailExecutorService;
+ private MailSenderInternalExecutorService mailExecutorService;
@Autowired
private PasswordResetExecutorService passwordResetExecutorService;
@Autowired
- private TbMailContextComponent tbMailContextComponent;
+ private TbMailContextComponent ctx;
+
+ @Autowired
+ private RateLimitService rateLimitService;
+
+ @Value("${mail.per_tenant_rate_limits:}")
+ private String perTenantRateLimitConfig;
+
+ private final ScheduledExecutorService timeoutScheduler;
private TbMailSender mailSender;
@@ -95,6 +111,7 @@ public class DefaultMailService implements MailService {
this.freemarkerConfig = freemarkerConfig;
this.adminSettingsService = adminSettingsService;
this.apiUsageClient = apiUsageClient;
+ this.timeoutScheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog"));
}
@PostConstruct
@@ -102,12 +119,19 @@ public class DefaultMailService implements MailService {
updateMailConfiguration();
}
+ @PreDestroy
+ public void destroy() {
+ if (timeoutScheduler != null) {
+ timeoutScheduler.shutdownNow();
+ }
+ }
+
@Override
public void updateMailConfiguration() {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (settings != null) {
JsonNode jsonConfig = settings.getJsonValue();
- mailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
+ mailSender = new TbMailSender(ctx, jsonConfig);
mailFrom = jsonConfig.get("mailFrom").asText();
timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
} else {
@@ -122,7 +146,7 @@ public class DefaultMailService implements MailService {
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
- TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
+ TbMailSender testMailSender = new TbMailSender(ctx, jsonConfig);
String mailFrom = jsonConfig.get("mailFrom").asText();
String subject = messages.getMessage("test.message.subject", null, Locale.US);
long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
@@ -214,6 +238,10 @@ public class DefaultMailService implements MailService {
private void sendMail(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender, long timeout) throws ThingsboardException {
if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) {
+ if (tenantId != null && !tenantId.isSysTenantId() && StringUtils.isNotEmpty(perTenantRateLimitConfig) &&
+ !rateLimitService.checkRateLimit(LimitedApi.EMAILS, (Object) tenantId, perTenantRateLimitConfig)) {
+ throw new RateLimitExceededException(LimitedApi.EMAILS);
+ }
try {
MimeMessage mailMsg = javaMailSender.createMimeMessage();
boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty());
@@ -415,8 +443,11 @@ public class DefaultMailService implements MailService {
}
private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) {
+ var submittedMail = Futures.withTimeout(
+ mailExecutorService.submit(() -> mailSender.send(msg)),
+ timeout, TimeUnit.MILLISECONDS, timeoutScheduler);
try {
- mailExecutorService.submit(() -> mailSender.send(msg)).get(timeout, TimeUnit.MILLISECONDS);
+ submittedMail.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.debug("Error during mail submission", e);
throw new RuntimeException("Timeout!");
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java
new file mode 100644
index 0000000000..0c8feff6c5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.common.util.AbstractListeningExecutor;
+
+/**
+ * Executor have the sole purpose to send mails. It should be used only by Mail Service.
+ * For other purposes please use the MailExecutorService component
+ * */
+@Component
+public class MailSenderInternalExecutorService extends AbstractListeningExecutor {
+
+ @Value("${actors.rule.mail_thread_pool_size}")
+ private int mailExecutorThreadPoolSize;
+
+ @Override
+ protected int getThreadPollSize() {
+ return mailExecutorThreadPoolSize;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
index 4df152936f..97c20fdcda 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@Data
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 42c516a8a3..fbef0cfd29 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
@@ -20,12 +20,12 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
+import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
@@ -78,10 +79,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
-import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
@@ -89,6 +91,7 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
+import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.resource.TbImageService;
@@ -109,7 +112,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -122,9 +124,11 @@ import java.util.stream.Collectors;
public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbCoreConsumerService {
@Value("${queue.core.poll-interval}")
- private long pollDuration;
+ private long pollInterval;
@Value("${queue.core.pack-processing-timeout}")
private long packProcessingTimeout;
+ @Value("${queue.core.consumer-per-partition:true}")
+ private boolean consumerPerPartition;
@Value("${queue.core.stats.enabled:false}")
private boolean statsEnabled;
@@ -133,7 +137,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> mainConsumer;
private final DeviceStateService stateService;
private final TbApiUsageStateService statsService;
private final TbLocalSubscriptionService localSubscriptionService;
@@ -144,14 +147,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer;
- private final TbQueueConsumer> firmwareStatesConsumer;
+ private final TbCoreQueueFactory queueFactory;
private final TbImageService imageService;
+ private final TbCoreConsumerStats stats;
+
+ private MainQueueConsumerManager, CoreQueueConfig> mainConsumer;
+ private QueueConsumerManager> usageStatsConsumer;
+ private QueueConsumerManager> firmwareStatesConsumer;
- protected volatile ExecutorService consumersExecutor;
- protected volatile ExecutorService usageStatsExecutor;
- private volatile ExecutorService firmwareStatesExecutor;
private volatile ListeningExecutorService deviceActivityEventsExecutor;
public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory,
@@ -176,10 +179,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder()
+ .queueKey(new QueueKey(ServiceType.TB_CORE))
+ .config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval))
+ .msgPackProcessor(this::processMsgs)
+ .consumerCreator(config -> queueFactory.createToCoreMsgConsumer())
+ .consumerExecutor(consumersExecutor)
+ .scheduler(scheduler)
+ .taskExecutor(mgmtExecutor)
+ .build();
+ this.usageStatsConsumer = QueueConsumerManager.>builder()
+ .name("TB Usage Stats")
+ .msgPackProcessor(this::processUsageStatsMsg)
+ .pollInterval(pollInterval)
+ .consumerCreator(queueFactory::createToUsageStatsServiceMsgConsumer)
+ .consumerExecutor(consumersExecutor)
+ .threadPrefix("usage-stats")
+ .build();
+ this.firmwareStatesConsumer = QueueConsumerManager.>builder()
+ .name("TB Ota Package States")
+ .msgPackProcessor(this::processFirmwareMsgs)
+ .pollInterval(pollInterval)
+ .consumerCreator(queueFactory::createToOtaPackageStateServiceMsgConsumer)
+ .consumerExecutor(consumersExecutor)
+ .threadPrefix("firmware")
+ .build();
}
@PreDestroy
public void destroy() {
super.destroy();
- if (consumersExecutor != null) {
- consumersExecutor.shutdownNow();
- }
- if (usageStatsExecutor != null) {
- usageStatsExecutor.shutdownNow();
- }
- if (firmwareStatesExecutor != null) {
- firmwareStatesExecutor.shutdownNow();
- }
if (deviceActivityEventsExecutor != null) {
deviceActivityEventsExecutor.shutdownNow();
}
}
- @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
- public void onApplicationEvent(ApplicationReadyEvent event) {
- super.onApplicationEvent(event);
- launchUsageStatsConsumer();
- launchOtaPackageUpdateNotificationConsumer();
+ @Override
+ protected void startConsumers() {
+ super.startConsumers();
+ firmwareStatesConsumer.subscribe();
+ firmwareStatesConsumer.launch();
+ usageStatsConsumer.launch();
}
@Override
protected void onTbApplicationEvent(PartitionChangeEvent event) {
log.info("Subscribing to partitions: {}", event.getPartitions());
- this.mainConsumer.subscribe(event.getPartitions());
- this.usageStatsConsumer.subscribe(
- event
- .getPartitions()
- .stream()
- .map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic()))
- .collect(Collectors.toSet()));
- this.firmwareStatesConsumer.subscribe();
- }
-
- @Override
- protected void launchMainConsumers() {
- consumersExecutor.submit(() -> {
- while (!stopped) {
+ mainConsumer.update(event.getPartitions());
+ usageStatsConsumer.subscribe(event.getPartitions()
+ .stream()
+ .map(tpi -> tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic()))
+ .collect(Collectors.toSet()));
+ }
+
+ private void processMsgs(List> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception {
+ List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
+ ConcurrentMap> pendingMap = orderedMsgList.stream().collect(
+ Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
+ CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
+ TbPackProcessingContext> ctx = new TbPackProcessingContext<>(
+ processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
+ PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
+ Future> packSubmitFuture = consumersExecutor.submit(() -> {
+ orderedMsgList.forEach((element) -> {
+ UUID id = element.getUuid();
+ TbProtoQueueMsg msg = element.getMsg();
+ log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
+ TbCallback callback = new TbPackCallback<>(id, ctx);
try {
- List> msgs = mainConsumer.poll(pollDuration);
- if (msgs.isEmpty()) {
- continue;
- }
- List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
- ConcurrentMap> pendingMap = orderedMsgList.stream().collect(
- Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
- CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
- TbPackProcessingContext> ctx = new TbPackProcessingContext<>(
- processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
- PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
- Future> packSubmitFuture = consumersExecutor.submit(() -> {
- orderedMsgList.forEach((element) -> {
- UUID id = element.getUuid();
- TbProtoQueueMsg msg = element.getMsg();
- log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
- TbCallback callback = new TbPackCallback<>(id, ctx);
- try {
- ToCoreMsg toCoreMsg = msg.getValue();
- pendingMsgHolder.setToCoreMsg(toCoreMsg);
- if (toCoreMsg.hasToSubscriptionMgrMsg()) {
- log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
- forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
- } else if (toCoreMsg.hasToDeviceActorMsg()) {
- log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
- forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
- } else if (toCoreMsg.hasDeviceStateServiceMsg()) {
- log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
- forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
- } else if (toCoreMsg.hasEdgeNotificationMsg()) {
- log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
- forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
- } else if (toCoreMsg.hasDeviceConnectMsg()) {
- log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg());
- forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback);
- } else if (toCoreMsg.hasDeviceActivityMsg()) {
- log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg());
- forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback);
- } else if (toCoreMsg.hasDeviceDisconnectMsg()) {
- log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg());
- forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback);
- } else if (toCoreMsg.hasDeviceInactivityMsg()) {
- log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg());
- forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback);
- } else if (toCoreMsg.hasToDeviceActorNotification()) {
- TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification());
- if (actorMsg != null) {
- if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
- tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg);
- } else {
- log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);
- actorContext.tell(actorMsg);
- }
- }
- callback.onSuccess();
- } else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) {
- TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
- log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
- forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
- } else if (toCoreMsg.hasErrorEventMsg()) {
- forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
- } else if (toCoreMsg.hasLifecycleEventMsg()) {
- forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
- }
- } catch (Throwable e) {
- log.warn("[{}] Failed to process message: {}", id, msg, e);
- callback.onFailure(e);
+ ToCoreMsg toCoreMsg = msg.getValue();
+ pendingMsgHolder.setToCoreMsg(toCoreMsg);
+ if (toCoreMsg.hasToSubscriptionMgrMsg()) {
+ log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
+ forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
+ } else if (toCoreMsg.hasToDeviceActorMsg()) {
+ log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
+ forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
+ } else if (toCoreMsg.hasDeviceStateServiceMsg()) {
+ log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
+ forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
+ } else if (toCoreMsg.hasEdgeNotificationMsg()) {
+ log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
+ forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
+ } else if (toCoreMsg.hasDeviceConnectMsg()) {
+ log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg());
+ forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback);
+ } else if (toCoreMsg.hasDeviceActivityMsg()) {
+ log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg());
+ forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback);
+ } else if (toCoreMsg.hasDeviceDisconnectMsg()) {
+ log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg());
+ forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback);
+ } else if (toCoreMsg.hasDeviceInactivityMsg()) {
+ log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg());
+ forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback);
+ } else if (toCoreMsg.hasToDeviceActorNotification()) {
+ TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification());
+ if (actorMsg != null) {
+ if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
+ tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg);
+ } else {
+ log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);
+ actorContext.tell(actorMsg);
}
- });
- });
- if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
- if (!packSubmitFuture.isDone()) {
- packSubmitFuture.cancel(true);
- ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg();
- log.info("Timeout to process message: {}", lastSubmitMsg);
- }
- ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
- ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
- }
- mainConsumer.commit();
- } catch (Exception e) {
- if (!stopped) {
- log.warn("Failed to obtain messages from queue.", e);
- try {
- Thread.sleep(pollDuration);
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new requests", e2);
}
+ callback.onSuccess();
+ } else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) {
+ TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
+ log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
+ forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
+ } else if (toCoreMsg.hasErrorEventMsg()) {
+ forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
+ } else if (toCoreMsg.hasLifecycleEventMsg()) {
+ forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
}
+ } catch (Throwable e) {
+ log.warn("[{}] Failed to process message: {}", id, msg, e);
+ callback.onFailure(e);
}
- }
- log.info("TB Core Consumer stopped.");
+ });
});
+ if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
+ if (!packSubmitFuture.isDone()) {
+ packSubmitFuture.cancel(true);
+ ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg();
+ log.info("Timeout to process message: {}", lastSubmitMsg);
+ }
+ if (log.isDebugEnabled()) {
+ ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
+ }
+ ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
+ }
+ consumer.commit();
}
private static class PendingMsgHolder {
@@ -353,7 +347,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> createNotificationsConsumer() {
+ return queueFactory.createToCoreNotificationsMsgConsumer();
+ }
+
@Override
protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) {
ToCoreNotificationMsg toCoreNotification = msg.getValue();
@@ -409,92 +413,55 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService {
- while (!stopped) {
- try {
- List> msgs = usageStatsConsumer.poll(getNotificationPollDuration());
- if (msgs.isEmpty()) {
- continue;
- }
- ConcurrentMap> pendingMap = msgs.stream().collect(
- Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity()));
- CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
- TbPackProcessingContext> ctx = new TbPackProcessingContext<>(
- processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
- pendingMap.forEach((id, msg) -> {
- log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue());
- TbCallback callback = new TbPackCallback<>(id, ctx);
- try {
- handleUsageStats(msg, callback);
- } catch (Throwable e) {
- log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
- callback.onFailure(e);
- }
- });
- if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
- ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue()));
- ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue()));
- }
- usageStatsConsumer.commit();
- } catch (Exception e) {
- if (!stopped) {
- log.warn("Failed to obtain usage stats from queue.", e);
- try {
- Thread.sleep(getNotificationPollDuration());
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new usage stats", e2);
- }
- }
- }
+ private void processUsageStatsMsg(List> msgs, TbQueueConsumer> consumer) throws Exception {
+ ConcurrentMap> pendingMap = msgs.stream().collect(
+ Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity()));
+ CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
+ TbPackProcessingContext> ctx = new TbPackProcessingContext<>(
+ processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
+ pendingMap.forEach((id, msg) -> {
+ log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue());
+ TbCallback callback = new TbPackCallback<>(id, ctx);
+ try {
+ handleUsageStats(msg, callback);
+ } catch (Throwable e) {
+ log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
+ callback.onFailure(e);
}
- log.info("TB Usage Stats Consumer stopped.");
});
+ if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
+ ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue()));
+ ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue()));
+ }
+ consumer.commit();
+
}
- private void launchOtaPackageUpdateNotificationConsumer() {
+ private void processFirmwareMsgs(List> msgs, TbQueueConsumer> consumer) {
long maxProcessingTimeoutPerRecord = firmwarePackInterval / firmwarePackSize;
- firmwareStatesExecutor.submit(() -> {
- while (!stopped) {
- try {
- List> msgs = firmwareStatesConsumer.poll(getNotificationPollDuration());
- if (msgs.isEmpty()) {
- continue;
- }
- long timeToSleep = maxProcessingTimeoutPerRecord;
- for (TbProtoQueueMsg msg : msgs) {
- try {
- long startTime = System.currentTimeMillis();
- boolean isSuccessUpdate = handleOtaPackageUpdates(msg);
- long endTime = System.currentTimeMillis();
- long spentTime = endTime - startTime;
- timeToSleep = timeToSleep - spentTime;
- if (isSuccessUpdate) {
- if (timeToSleep > 0) {
- log.debug("Spent time per record is: [{}]!", spentTime);
- Thread.sleep(timeToSleep);
- timeToSleep = 0;
- }
- timeToSleep += maxProcessingTimeoutPerRecord;
- }
- } catch (Throwable e) {
- log.warn("Failed to process firmware update msg: {}", msg, e);
- }
- }
- firmwareStatesConsumer.commit();
- } catch (Exception e) {
- if (!stopped) {
- log.warn("Failed to obtain usage stats from queue.", e);
- try {
- Thread.sleep(getNotificationPollDuration());
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new firmware updates", e2);
- }
+ long timeToSleep = maxProcessingTimeoutPerRecord;
+ for (TbProtoQueueMsg msg : msgs) {
+ try {
+ long startTime = System.currentTimeMillis();
+ boolean isSuccessUpdate = handleOtaPackageUpdates(msg);
+ long endTime = System.currentTimeMillis();
+ long spentTime = endTime - startTime;
+ timeToSleep = timeToSleep - spentTime;
+ if (isSuccessUpdate) {
+ if (timeToSleep > 0) {
+ log.debug("Spent time per record is: [{}]!", spentTime);
+ Thread.sleep(timeToSleep);
+ timeToSleep = 0;
}
+ timeToSleep += maxProcessingTimeoutPerRecord;
}
+ } catch (InterruptedException e) {
+ return;
+ } catch (Throwable e) {
+ log.warn("Failed to process firmware update msg: {}", msg, e);
}
- log.info("TB Ota Package States Consumer stopped.");
- });
+ }
+ consumer.commit();
}
private void handleUsageStats(TbProtoQueueMsg msg, TbCallback callback) {
@@ -782,15 +749,17 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService consumers = new ConcurrentHashMap<>();
public DefaultTbRuleEngineConsumerService(TbRuleEngineConsumerContext ctx,
- TbRuleEngineQueueFactory tbRuleEngineQueueFactory,
ActorSystemContext actorContext,
TbRuleEngineDeviceRpcService tbDeviceRpcService,
QueueService queueService,
@@ -88,8 +84,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
- super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
- eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), jwtSettingsService);
+ super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;
@@ -97,7 +92,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
@PostConstruct
public void init() {
- super.init("tb-rule-engine-notifications-consumer");
+ super.init("tb-rule-engine");
List queues = queueService.findAllQueues();
for (Queue configuration : queues) {
if (partitionService.isManagedByCurrentService(configuration.getTenantId())) {
@@ -137,20 +132,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
});
}
- @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
- public void onApplicationEvent(ApplicationReadyEvent event) {
- super.onApplicationEvent(event);
- ctx.setReady(true);
- }
-
- @Override
- protected void launchMainConsumers() {}
-
@Override
protected void stopConsumers() {
+ super.stopConsumers();
consumers.values().forEach(TbRuleEngineQueueConsumerManager::stop);
consumers.values().forEach(TbRuleEngineQueueConsumerManager::awaitStop);
- ctx.stop();
}
@Override
@@ -168,6 +154,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
return ctx.getPackProcessingTimeout();
}
+ @Override
+ protected int getMgmtThreadPoolSize() {
+ return ctx.getMgmtThreadPoolSize();
+ }
+
+ @Override
+ protected TbQueueConsumer> createNotificationsConsumer() {
+ return ctx.getQueueFactory().createToRuleEngineNotificationsMsgConsumer();
+ }
+
@Override
protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception {
ToRuleEngineNotificationMsg nfMsg = msg.getValue();
@@ -244,7 +240,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
}
private TbRuleEngineQueueConsumerManager createConsumer(QueueKey queueKey, Queue queue) {
- var consumer = new TbRuleEngineQueueConsumerManager(ctx, queueKey);
+ var consumer = TbRuleEngineQueueConsumerManager.create()
+ .ctx(ctx)
+ .queueKey(queueKey)
+ .consumerExecutor(consumersExecutor)
+ .scheduler(scheduler)
+ .taskExecutor(mgmtExecutor)
+ .build();
consumers.put(queueKey, consumer);
consumer.init(queue);
return consumer;
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java
new file mode 100644
index 0000000000..6792422b57
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java
@@ -0,0 +1,322 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.queue.consumer;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.common.data.queue.QueueConfig;
+import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.queue.TbQueueConsumer;
+import org.thingsboard.server.queue.TbQueueMsg;
+import org.thingsboard.server.queue.discovery.QueueKey;
+import org.thingsboard.server.service.queue.ruleengine.QueueEvent;
+import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask;
+import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class MainQueueConsumerManager {
+
+ protected final QueueKey queueKey;
+ @Getter
+ protected C config;
+ protected final MsgPackProcessor msgPackProcessor;
+ protected final Function> consumerCreator;
+ protected final ExecutorService consumerExecutor;
+ protected final ScheduledExecutorService scheduler;
+ protected final ExecutorService taskExecutor;
+
+ private final java.util.Queue tasks = new ConcurrentLinkedQueue<>();
+ private final ReentrantLock lock = new ReentrantLock();
+
+ @Getter
+ private volatile Set partitions;
+ protected volatile ConsumerWrapper consumerWrapper;
+ protected volatile boolean stopped;
+
+ @Builder
+ public MainQueueConsumerManager(QueueKey queueKey, C config,
+ MsgPackProcessor msgPackProcessor,
+ Function> consumerCreator,
+ ExecutorService consumerExecutor,
+ ScheduledExecutorService scheduler,
+ ExecutorService taskExecutor) {
+ this.queueKey = queueKey;
+ this.config = config;
+ this.msgPackProcessor = msgPackProcessor;
+ this.consumerCreator = consumerCreator;
+ this.consumerExecutor = consumerExecutor;
+ this.scheduler = scheduler;
+ this.taskExecutor = taskExecutor;
+ if (config != null) {
+ init(config);
+ }
+ }
+
+ public void init(C config) {
+ this.config = config;
+ if (config.isConsumerPerPartition()) {
+ this.consumerWrapper = new ConsumerPerPartitionWrapper();
+ } else {
+ this.consumerWrapper = new SingleConsumerWrapper();
+ }
+ log.debug("[{}] Initialized consumer for queue: {}", queueKey, config);
+ }
+
+ public void update(C config) {
+ addTask(TbQueueConsumerManagerTask.configUpdate(config));
+ }
+
+ public void update(Set partitions) {
+ addTask(TbQueueConsumerManagerTask.partitionChange(partitions));
+ }
+
+ protected void addTask(TbQueueConsumerManagerTask todo) {
+ if (stopped) {
+ return;
+ }
+ tasks.add(todo);
+ log.trace("[{}] Added task: {}", queueKey, todo);
+ tryProcessTasks();
+ }
+
+ private void tryProcessTasks() {
+ taskExecutor.submit(() -> {
+ if (lock.tryLock()) {
+ try {
+ C newConfig = null;
+ Set newPartitions = null;
+ while (!stopped) {
+ TbQueueConsumerManagerTask task = tasks.poll();
+ if (task == null) {
+ break;
+ }
+ log.trace("[{}] Processing task: {}", queueKey, task);
+
+ if (task.getEvent() == QueueEvent.PARTITION_CHANGE) {
+ newPartitions = task.getPartitions();
+ } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) {
+ newConfig = (C) task.getConfig();
+ } else {
+ processTask(task);
+ }
+ }
+ if (stopped) {
+ return;
+ }
+ if (newConfig != null) {
+ doUpdate(newConfig);
+ }
+ if (newPartitions != null) {
+ doUpdate(newPartitions);
+ }
+ } catch (Exception e) {
+ log.error("[{}] Failed to process tasks", queueKey, e);
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ log.trace("[{}] Failed to acquire lock", queueKey);
+ scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
+ }
+ });
+ }
+
+ protected void processTask(TbQueueConsumerManagerTask task) {
+ }
+
+ private void doUpdate(C newConfig) {
+ log.info("[{}] Processing queue update: {}", queueKey, newConfig);
+ var oldConfig = this.config;
+ this.config = newConfig;
+ if (log.isTraceEnabled()) {
+ log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig);
+ log.trace("[{}] New queue configuration: {}", queueKey, newConfig);
+ }
+
+ if (oldConfig == null) {
+ init(config);
+ } else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) {
+ consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
+ consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
+
+ init(config);
+ if (partitions != null) {
+ doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
+ }
+ } else {
+ log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey);
+ // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
+ // and changes to other config values will be picked up by consumer on the fly,
+ // and queue topic and name are immutable
+ }
+ }
+
+ private void doUpdate(Set partitions) {
+ this.partitions = partitions;
+ consumerWrapper.updatePartitions(partitions);
+ }
+
+ private void launchConsumer(TbQueueConsumerTask consumerTask) {
+ log.info("[{}] Launching consumer", consumerTask.getKey());
+ Future> consumerLoop = consumerExecutor.submit(() -> {
+ ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString());
+ try {
+ consumerLoop(consumerTask.getConsumer());
+ } catch (Throwable e) {
+ log.error("Failure in consumer loop", e);
+ }
+ log.info("[{}] Consumer stopped", consumerTask.getKey());
+ });
+ consumerTask.setTask(consumerLoop);
+ }
+
+ private void consumerLoop(TbQueueConsumer consumer) {
+ while (!stopped && !consumer.isStopped()) {
+ try {
+ List msgs = consumer.poll(config.getPollInterval());
+ if (msgs.isEmpty()) {
+ continue;
+ }
+ processMsgs(msgs, consumer, config);
+ } catch (Exception e) {
+ if (!consumer.isStopped()) {
+ log.warn("Failed to process messages from queue", e);
+ try {
+ Thread.sleep(config.getPollInterval());
+ } catch (InterruptedException e2) {
+ log.trace("Failed to wait until the server has capacity to handle new requests", e2);
+ }
+ }
+ }
+ }
+ if (consumer.isStopped()) {
+ consumer.unsubscribe();
+ }
+ }
+
+ protected void processMsgs(List msgs, TbQueueConsumer consumer, C config) throws Exception {
+ msgPackProcessor.process(msgs, consumer, config);
+ }
+
+ public void stop() {
+ log.debug("[{}] Stopping consumers", queueKey);
+ consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
+ stopped = true;
+ }
+
+ public void awaitStop() {
+ log.debug("[{}] Waiting for consumers to stop", queueKey);
+ consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
+ log.debug("[{}] Unsubscribed and stopped consumers", queueKey);
+ }
+
+ private static String partitionsToString(Collection partitions) {
+ return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ public interface MsgPackProcessor {
+ void process(List msgs, TbQueueConsumer consumer, C config) throws Exception;
+ }
+
+ public interface ConsumerWrapper {
+
+ void updatePartitions(Set partitions);
+
+ Collection> getConsumers();
+
+ }
+
+ class ConsumerPerPartitionWrapper implements ConsumerWrapper {
+ private final Map> consumers = new HashMap<>();
+
+ @Override
+ public void updatePartitions(Set partitions) {
+ Set addedPartitions = new HashSet<>(partitions);
+ addedPartitions.removeAll(consumers.keySet());
+
+ Set removedPartitions = new HashSet<>(consumers.keySet());
+ removedPartitions.removeAll(partitions);
+ log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions));
+
+ removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop());
+ removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion());
+
+ addedPartitions.forEach((tpi) -> {
+ String key = queueKey + "-" + tpi.getPartition().orElse(-1);
+ TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config));
+ consumers.put(tpi, consumer);
+ consumer.subscribe(Set.of(tpi));
+ launchConsumer(consumer);
+ });
+ }
+
+ @Override
+ public Collection> getConsumers() {
+ return consumers.values();
+ }
+ }
+
+ class SingleConsumerWrapper implements ConsumerWrapper {
+ private TbQueueConsumerTask consumer;
+
+ @Override
+ public void updatePartitions(Set partitions) {
+ log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions));
+ if (partitions.isEmpty()) {
+ if (consumer != null && consumer.isRunning()) {
+ consumer.initiateStop();
+ consumer.awaitCompletion();
+ }
+ consumer = null;
+ return;
+ }
+
+ if (consumer == null) {
+ consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config));
+ }
+ consumer.subscribe(partitions);
+ if (!consumer.isRunning()) {
+ launchConsumer(consumer);
+ }
+ }
+
+ @Override
+ public Collection> getConsumers() {
+ if (consumer == null) {
+ return Collections.emptyList();
+ }
+ return List.of(consumer);
+ }
+ }
+}
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 fe15b457f2..a264f912cb 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,10 +15,11 @@
*/
package org.thingsboard.server.service.queue.processing;
+import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
+import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
@@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@@ -47,9 +49,6 @@ import org.thingsboard.server.service.queue.TbPackCallback;
import org.thingsboard.server.service.queue.TbPackProcessingContext;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
-import jakarta.annotation.PreDestroy;
-import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
-
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@@ -57,6 +56,7 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -64,9 +64,6 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public abstract class AbstractConsumerService extends TbApplicationEventListener {
- protected volatile ExecutorService notificationsConsumerExecutor;
- protected volatile boolean stopped = false;
- protected volatile boolean isReady = false;
protected final ActorSystemContext actorContext;
protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache;
@@ -74,21 +71,37 @@ public abstract class AbstractConsumerService> nfConsumer;
protected final JwtSettingsService jwtSettingsService;
- public void init(String nfConsumerThreadName) {
- this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName));
+ protected QueueConsumerManager> nfConsumer;
+
+ protected ExecutorService consumersExecutor;
+ protected ExecutorService mgmtExecutor;
+ protected ScheduledExecutorService scheduler;
+
+ public void init(String prefix) {
+ this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(prefix + "-consumer"));
+ this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(getMgmtThreadPoolSize(), prefix + "-mgmt");
+ this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName(prefix + "-consumer-scheduler"));
+
+ this.nfConsumer = QueueConsumerManager.>builder()
+ .name(getServiceType().getLabel() + " Notifications")
+ .msgPackProcessor(this::processNotifications)
+ .pollInterval(getNotificationPollDuration())
+ .consumerCreator(this::createNotificationsConsumer)
+ .consumerExecutor(consumersExecutor)
+ .threadPrefix("notifications")
+ .build();
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
- public void onApplicationEvent(ApplicationReadyEvent event) {
- log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
- this.nfConsumer.subscribe();
- this.isReady = true;
- launchNotificationsConsumer();
- launchMainConsumers();
+ public void afterStartUp() {
+ startConsumers();
+ }
+
+ protected void startConsumers() {
+ nfConsumer.subscribe();
+ nfConsumer.launch();
}
@Override
@@ -98,58 +111,42 @@ public abstract class AbstractConsumerService {
- while (!stopped) {
- try {
- List> msgs = nfConsumer.poll(getNotificationPollDuration());
- if (msgs.isEmpty()) {
- continue;
- }
- List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
- ConcurrentMap> pendingMap = orderedMsgList.stream().collect(
- Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
- CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
- TbPackProcessingContext> ctx = new TbPackProcessingContext<>(
- processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
- orderedMsgList.forEach(element -> {
- UUID id = element.getUuid();
- TbProtoQueueMsg msg = element.getMsg();
- log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue());
- TbCallback callback = new TbPackCallback<>(id, ctx);
- try {
- handleNotification(id, msg, callback);
- } catch (Throwable e) {
- log.warn("[{}] Failed to process notification: {}", id, msg, e);
- callback.onFailure(e);
- }
- });
- if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
- ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue()));
- ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue()));
- }
- nfConsumer.commit();
- } catch (Exception e) {
- if (!stopped) {
- log.warn("Failed to obtain notifications from queue.", e);
- try {
- Thread.sleep(getNotificationPollDuration());
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new notifications", e2);
- }
- }
- }
+ protected abstract int getMgmtThreadPoolSize();
+
+ protected abstract TbQueueConsumer> createNotificationsConsumer();
+
+ protected void processNotifications(List> msgs, TbQueueConsumer> consumer) throws Exception {
+ List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
+ ConcurrentMap> pendingMap = orderedMsgList.stream().collect(
+ Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
+ CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
+ TbPackProcessingContext> ctx = new TbPackProcessingContext<>(
+ processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
+ orderedMsgList.forEach(element -> {
+ UUID id = element.getUuid();
+ TbProtoQueueMsg msg = element.getMsg();
+ log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue());
+ TbCallback callback = new TbPackCallback<>(id, ctx);
+ try {
+ handleNotification(id, msg, callback);
+ } catch (Throwable e) {
+ log.warn("[{}] Failed to process notification: {}", id, msg, e);
+ callback.onFailure(e);
}
- log.info("TB Notifications Consumer stopped.");
});
+ if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
+ ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue()));
+ ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue()));
+ }
+ consumer.commit();
}
protected final void handleComponentLifecycleMsg(UUID id, ComponentLifecycleMsg componentLifecycleMsg) {
@@ -203,13 +200,15 @@ public abstract class AbstractConsumerService partitions;
private boolean drainQueue;
@@ -37,8 +37,8 @@ public class TbQueueConsumerManagerTask {
return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue);
}
- public static TbQueueConsumerManagerTask configUpdate(Queue queue) {
- return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, queue, null, false);
+ public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) {
+ return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false);
}
public static TbQueueConsumerManagerTask partitionChange(Set partitions) {
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java
index 0e25efd014..84539d4d7c 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java
@@ -20,9 +20,8 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
-import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueConsumer;
-import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.TbQueueMsg;
import java.util.Set;
import java.util.concurrent.Future;
@@ -30,12 +29,12 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
@Slf4j
-public class TbQueueConsumerTask {
+public class TbQueueConsumerTask {
@Getter
private final Object key;
@Getter
- private final TbQueueConsumer> consumer;
+ private final TbQueueConsumer consumer;
@Setter
private Future> task;
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java
index cf876c6947..08d2931246 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java
@@ -19,8 +19,6 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
-import org.thingsboard.common.util.ThingsBoardExecutors;
-import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.queue.TbQueueAdmin;
@@ -33,11 +31,6 @@ import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStr
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
-import javax.annotation.PostConstruct;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-
@Component
@TbRuleEngineComponent
@Slf4j
@@ -68,22 +61,4 @@ public class TbRuleEngineConsumerContext {
private final TbQueueProducerProvider producerProvider;
private final TbQueueAdmin queueAdmin;
- private ExecutorService consumersExecutor;
- private ExecutorService mgmtExecutor;
- private ScheduledExecutorService scheduler;
-
- private volatile boolean isReady = false;
-
- @PostConstruct
- void init() {
- this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer"));
- this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(mgmtThreadPoolSize, "tb-rule-engine-mgmt");
- this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler"));
- }
-
- public void stop() {
- scheduler.shutdownNow();
- consumersExecutor.shutdownNow();
- mgmtExecutor.shutdownNow();
- }
}
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 2da3dbc6dc..c2823d3c00 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
@@ -16,9 +16,8 @@
package org.thingsboard.server.service.queue.ruleengine;
import com.google.protobuf.ProtocolStringList;
-import lombok.Getter;
+import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
@@ -38,173 +37,54 @@ import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.queue.TbMsgPackCallback;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContext;
import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats;
+import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
-import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Future;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j
-public class TbRuleEngineQueueConsumerManager {
+public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager, Queue> {
public static final String SUCCESSFUL_STATUS = "successful";
public static final String FAILED_STATUS = "failed";
private final TbRuleEngineConsumerContext ctx;
- private final QueueKey queueKey;
private final TbRuleEngineConsumerStats stats;
- private final ReentrantLock lock = new ReentrantLock(); //NonfairSync
- @Getter
- private volatile Queue queue;
- @Getter
- private volatile Set partitions;
- private volatile ConsumerWrapper consumerWrapper;
-
- private volatile boolean stopped;
-
- private final java.util.Queue tasks = new ConcurrentLinkedQueue<>();
-
- public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, QueueKey queueKey) {
+ @Builder(builderMethodName = "create") // not to conflict with super.builder()
+ public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx,
+ QueueKey queueKey,
+ ExecutorService consumerExecutor,
+ ScheduledExecutorService scheduler,
+ ExecutorService taskExecutor) {
+ super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor);
this.ctx = ctx;
- this.queueKey = queueKey;
this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory());
}
- public void init(Queue queue) {
- this.queue = queue;
- if (queue.isConsumerPerPartition()) {
- this.consumerWrapper = new ConsumerPerPartitionWrapper();
- } else {
- this.consumerWrapper = new SingleConsumerWrapper();
- }
- log.debug("[{}] Initialized consumer for queue: {}", queueKey, queue);
- }
-
- public void update(Queue queue) {
- addTask(TbQueueConsumerManagerTask.configUpdate(queue));
- }
-
- public void update(Set partitions) {
- addTask(TbQueueConsumerManagerTask.partitionChange(partitions));
- }
-
public void delete(boolean drainQueue) {
addTask(TbQueueConsumerManagerTask.delete(drainQueue));
}
- private void addTask(TbQueueConsumerManagerTask todo) {
- if (stopped) {
- return;
- }
- tasks.add(todo);
- log.trace("[{}] Added task: {}", queueKey, todo);
- tryProcessTasks();
- }
-
- private void tryProcessTasks() {
- if (!ctx.isReady()) {
- log.debug("[{}] TbRuleEngineConsumerContext is not ready yet, will process tasks later", queueKey);
- ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
- return;
- }
- ctx.getMgmtExecutor().submit(() -> {
- if (lock.tryLock()) {
- try {
- Queue newConfiguration = null;
- Set newPartitions = null;
- while (!stopped) {
- TbQueueConsumerManagerTask task = tasks.poll();
- if (task == null) {
- break;
- }
- log.trace("[{}] Processing task: {}", queueKey, task);
-
- if (task.getEvent() == QueueEvent.PARTITION_CHANGE) {
- newPartitions = task.getPartitions();
- } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) {
- newConfiguration = task.getQueue();
- } else if (task.getEvent() == QueueEvent.DELETE) {
- doDelete(task.isDrainQueue());
- return;
- }
- }
- if (stopped) {
- return;
- }
- if (newConfiguration != null) {
- doUpdate(newConfiguration);
- }
- if (newPartitions != null) {
- doUpdate(newPartitions);
- }
- } catch (Exception e) {
- log.error("[{}] Failed to process tasks", queueKey, e);
- } finally {
- lock.unlock();
- }
- } else {
- log.trace("[{}] Failed to acquire lock", queueKey);
- ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
- }
- });
- }
-
- private void doUpdate(Queue newQueue) {
- log.info("[{}] Processing queue update: {}", queueKey, newQueue);
- var oldQueue = this.queue;
- this.queue = newQueue;
- if (log.isTraceEnabled()) {
- log.trace("[{}] Old queue configuration: {}", queueKey, oldQueue);
- log.trace("[{}] New queue configuration: {}", queueKey, newQueue);
- }
-
- if (oldQueue == null) {
- init(queue);
- } else if (newQueue.isConsumerPerPartition() != oldQueue.isConsumerPerPartition()) {
- consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
- consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
-
- init(queue);
- if (partitions != null) {
- doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
- }
- } else {
- // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
- // and changes to pollInterval/packProcessingTimeout/submitStrategy/processingStrategy will be picked up by consumer on the fly,
- // and queue topic and name are immutable
+ @Override
+ protected void processTask(TbQueueConsumerManagerTask task) {
+ if (task.getEvent() == QueueEvent.DELETE) {
+ doDelete(task.isDrainQueue());
}
}
- private void doUpdate(Set partitions) {
- this.partitions = partitions;
- consumerWrapper.updatePartitions(partitions);
- }
-
- public void stop() {
- log.debug("[{}] Stopping consumers", queueKey);
- consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
- stopped = true;
- }
-
- public void awaitStop() {
- consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
- log.debug("[{}] Unsubscribed and stopped consumers", queueKey);
- }
-
private void doDelete(boolean drainQueue) {
stopped = true;
log.info("[{}] Handling queue deletion", queueKey);
@@ -212,7 +92,7 @@ public class TbRuleEngineQueueConsumerManager {
List>> queueConsumers = consumerWrapper.getConsumers().stream()
.map(TbQueueConsumerTask::getConsumer).collect(Collectors.toList());
- ctx.getConsumersExecutor().submit(() -> {
+ consumerExecutor.submit(() -> {
if (drainQueue) {
drainQueue(queueConsumers);
}
@@ -235,47 +115,10 @@ public class TbRuleEngineQueueConsumerManager {
});
}
- private void launchConsumer(TbQueueConsumerTask consumerTask) {
- log.info("[{}] Launching consumer", consumerTask.getKey());
- Future> consumerLoop = ctx.getConsumersExecutor().submit(() -> {
- ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString());
- try {
- consumerLoop(consumerTask.getConsumer());
- } catch (Throwable e) {
- log.error("Failure in consumer loop", e);
- }
- });
- consumerTask.setTask(consumerLoop);
- }
-
- private void consumerLoop(TbQueueConsumer> consumer) {
- while (!stopped && !consumer.isStopped()) {
- try {
- List> msgs = consumer.poll(queue.getPollInterval());
- if (msgs.isEmpty()) {
- continue;
- }
- processMsgs(msgs, consumer, queue);
- } catch (Exception e) {
- if (!consumer.isStopped()) {
- log.warn("Failed to process messages from queue", e);
- try {
- Thread.sleep(ctx.getPollDuration());
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new requests", e2);
- }
- }
- }
- }
- if (consumer.isStopped()) {
- consumer.unsubscribe();
- }
- log.info("Rule Engine consumer stopped");
- }
-
- private void processMsgs(List> msgs,
- TbQueueConsumer> consumer,
- Queue queue) throws InterruptedException {
+ @Override
+ protected void processMsgs(List> msgs,
+ TbQueueConsumer> consumer,
+ Queue queue) throws Exception {
TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue);
TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue);
submitStrategy.init(msgs);
@@ -320,7 +163,7 @@ public class TbRuleEngineQueueConsumerManager {
}
private void submitMessage(TbMsgPackProcessingContext packCtx, UUID id, TbProtoQueueMsg msg) {
- log.trace("[{}] Creating callback for topic {} message: {}", id, queue.getName(), msg.getValue());
+ log.trace("[{}] Creating callback for topic {} message: {}", id, config.getName(), msg.getValue());
ToRuleEngineMsg toRuleEngineMsg = msg.getValue();
TenantId tenantId = TenantId.fromUUID(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB()));
TbMsgCallback callback = ctx.isPrometheusStatsEnabled() ?
@@ -328,7 +171,7 @@ public class TbRuleEngineQueueConsumerManager {
new TbMsgPackCallback(id, tenantId, packCtx);
try {
if (!toRuleEngineMsg.getTbMsg().isEmpty()) {
- forwardToRuleEngineActor(queue.getName(), tenantId, toRuleEngineMsg, callback);
+ forwardToRuleEngineActor(config.getName(), tenantId, toRuleEngineMsg, callback);
} else {
callback.onSuccess();
}
@@ -356,7 +199,7 @@ public class TbRuleEngineQueueConsumerManager {
log.info("[{}] {} to process [{}] messages", queueKey, prefix, map.size());
for (Map.Entry> pending : map.entrySet()) {
ToRuleEngineMsg tmp = pending.getValue().getValue();
- TbMsg tmpMsg = TbMsg.fromBytes(queue.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY);
+ TbMsg tmpMsg = TbMsg.fromBytes(config.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY);
RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey());
if (printAll) {
log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo);
@@ -379,7 +222,7 @@ public class TbRuleEngineQueueConsumerManager {
int n = 0;
while (System.currentTimeMillis() <= finishTs) {
for (TbQueueConsumer> consumer : consumers) {
- List> msgs = consumer.poll(queue.getPollInterval());
+ List> msgs = consumer.poll(config.getPollInterval());
if (msgs.isEmpty()) {
continue;
}
@@ -388,7 +231,7 @@ public class TbRuleEngineQueueConsumerManager {
MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray());
EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB()));
- TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, queue.getName(), TenantId.SYS_TENANT_ID, originator);
+ TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator);
ctx.getProducerProvider().getRuleEngineMsgProducer().send(tpi, msg, null);
n++;
} catch (Throwable e) {
@@ -399,90 +242,11 @@ public class TbRuleEngineQueueConsumerManager {
}
}
if (n > 0) {
- log.info("Moved {} messages from {} to system {}", n, queueKey, queue.getName());
+ log.info("Moved {} messages from {} to system {}", n, queueKey, config.getName());
}
} catch (Exception e) {
log.error("[{}] Failed to drain queue", queueKey, e);
}
}
- private static String partitionsToString(Collection partitions) {
- return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]"));
- }
-
- interface ConsumerWrapper {
-
- void updatePartitions(Set partitions);
-
- Collection getConsumers();
-
- }
-
- class ConsumerPerPartitionWrapper implements ConsumerWrapper {
- private final Map consumers = new HashMap<>();
-
- @Override
- public void updatePartitions(Set partitions) {
- Set addedPartitions = new HashSet<>(partitions);
- addedPartitions.removeAll(consumers.keySet());
-
- Set removedPartitions = new HashSet<>(consumers.keySet());
- removedPartitions.removeAll(partitions);
- log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions));
-
- removedPartitions.forEach((tpi) -> {
- consumers.get(tpi).initiateStop();
- });
- removedPartitions.forEach((tpi) -> {
- consumers.remove(tpi).awaitCompletion();
- });
-
- addedPartitions.forEach((tpi) -> {
- String key = queueKey + "-" + tpi.getPartition().orElse(-999999);
- TbQueueConsumerTask consumer = new TbQueueConsumerTask(key, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue));
- consumers.put(tpi, consumer);
- consumer.subscribe(Set.of(tpi));
- launchConsumer(consumer);
- });
- }
-
- @Override
- public Collection getConsumers() {
- return consumers.values();
- }
- }
-
- class SingleConsumerWrapper implements ConsumerWrapper {
- private TbQueueConsumerTask consumer;
-
- @Override
- public void updatePartitions(Set partitions) {
- log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions));
- if (partitions.isEmpty()) {
- if (consumer != null && consumer.isRunning()) {
- consumer.initiateStop();
- consumer.awaitCompletion();
- }
- consumer = null;
- return;
- }
-
- if (consumer == null) {
- consumer = new TbQueueConsumerTask(queueKey, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue));
- }
- consumer.subscribe(partitions);
- if (!consumer.isRunning()) {
- launchConsumer(consumer);
- }
- }
-
- @Override
- public Collection getConsumers() {
- if (consumer == null) {
- return Collections.emptyList();
- }
- return List.of(consumer);
- }
- }
-
}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index bfd470b611..25a2d617b7 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -164,6 +164,8 @@ mail:
oauth2:
# Interval for checking refresh token expiration in seconds(by default, 1 day).
refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}"
+ # Rate limits for sending mails per tenant. As example for 1000 per minute and 10000 per hour is "1000:60,10000:3600"
+ per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}"
# Usage statistics parameters
usage:
@@ -1633,6 +1635,8 @@ queue:
partitions: "${TB_QUEUE_CORE_PARTITIONS:10}"
# Timeout for processing a message pack by Core microservices
pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}"
+ # Enable/disable a separate consumer per partition for Core queue
+ consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}"
ota:
# Default topic name for OTA updates
topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}"
diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java
index 31c82408c1..55af29ab0b 100644
--- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java
@@ -92,7 +92,7 @@ public class HomePageApiTest extends AbstractControllerTest {
@MockBean
private SmsService smsService;
- private static final int DEFAULT_DASHBOARDS_COUNT = 1;
+ private static final int DEFAULT_DASHBOARDS_COUNT = 0;
//For system administrator
@Test
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 8e0b294241..50a61cc9e7 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
@@ -27,6 +27,8 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.common.util.ThingsBoardExecutors;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.QueueId;
@@ -65,6 +67,9 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@@ -86,7 +91,6 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -112,6 +116,9 @@ public class TbRuleEngineQueueConsumerManagerTest {
@Mock
private TbQueueAdmin queueAdmin;
private TbRuleEngineConsumerContext ruleEngineConsumerContext;
+ private ExecutorService consumersExecutor;
+ private ScheduledExecutorService scheduler;
+ private ExecutorService mgmtExecutor;
private TbRuleEngineQueueConsumerManager consumerManager;
private Queue queue;
@@ -141,10 +148,10 @@ public class TbRuleEngineQueueConsumerManagerTest {
}).when(actorContext).tell(any());
ruleEngineMsgProducer = mock(TbQueueProducer.class);
when(producerProvider.getRuleEngineMsgProducer()).thenReturn(ruleEngineMsgProducer);
- ruleEngineConsumerContext.setMgmtThreadPoolSize(2);
+ consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer"));
+ mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(3, "tb-rule-engine-mgmt");
+ scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler"));
ruleEngineConsumerContext.setTopicDeletionDelayInSec(5);
- ruleEngineConsumerContext.init();
- ruleEngineConsumerContext.setReady(false);
queue = new Queue();
queue.setName("Test");
@@ -174,14 +181,23 @@ public class TbRuleEngineQueueConsumerManagerTest {
}).when(queueFactory).createToRuleEngineMsgConsumer(any());
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue);
- consumerManager = new TbRuleEngineQueueConsumerManager(ruleEngineConsumerContext, queueKey);
+ consumerManager = TbRuleEngineQueueConsumerManager.create()
+ .ctx(ruleEngineConsumerContext)
+ .queueKey(queueKey)
+ .consumerExecutor(consumersExecutor)
+ .scheduler(scheduler)
+ .taskExecutor(mgmtExecutor)
+ .build();
}
@After
public void afterEach() {
consumerManager.stop();
consumerManager.awaitStop();
- ruleEngineConsumerContext.stop();
+
+ consumersExecutor.shutdownNow();
+ scheduler.shutdownNow();
+ mgmtExecutor.shutdownNow();
if (generateQueueMsgs) {
await().atMost(10, TimeUnit.SECONDS)
@@ -199,14 +215,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
Set partitions = createTpis(2, 3, 4);
consumerManager.update(partitions);
- partitions = createTpis(3, 4, 5);
- consumerManager.update(partitions);
- partitions = createTpis(1, 2, 3);
- consumerManager.update(partitions);
- // simulated multiple partition change events before consumer is ready; only latest partitions should be processed
- verifyNoInteractions(queueFactory);
- ruleEngineConsumerContext.setReady(true);
await().atMost(2, TimeUnit.SECONDS)
.until(() -> consumers.size() == 3);
for (TopicPartitionInfo partition : partitions) {
@@ -222,14 +231,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
Set partitions = createTpis(2, 3, 4);
consumerManager.update(partitions);
- partitions = createTpis(3, 4, 5);
- consumerManager.update(partitions);
- partitions = createTpis(1, 2, 3);
- consumerManager.update(partitions);
-
- verifyNoInteractions(queueFactory);
- ruleEngineConsumerContext.setReady(true);
await().atMost(2, TimeUnit.SECONDS)
.until(() -> consumers.size() == 1);
TestConsumer consumer = getConsumer();
@@ -240,7 +242,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testPartitionsUpdate_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = Collections.emptySet();
consumerManager.update(partitions);
@@ -273,7 +274,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testPartitionsUpdate_consumerPerPartition() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
consumerManager.update(Collections.emptySet());
verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any());
@@ -316,7 +316,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer = getConsumer();
@@ -342,7 +341,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_consumerPerPartition() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer1 = getConsumer(1);
@@ -375,7 +373,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_fromSingleToConsumerPerPartition() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer = getConsumer();
@@ -395,7 +392,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_fromConsumerPerPartitionToSingle() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer1 = getConsumer(1);
@@ -419,7 +415,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testStop() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
consumerManager.update(createTpis(1));
TestConsumer consumer = getConsumer(1);
verifySubscribedAndLaunched(consumer, 1);
@@ -437,7 +432,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testDelete_consumerPerPartition() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = createTpis(1, 2);
consumerManager.update(partitions);
TestConsumer consumer1 = getConsumer(1);
@@ -459,7 +453,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
int msgCount = totalConsumedMsgs.get();
await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call)
- .atMost(7, TimeUnit.SECONDS)
+ .atMost(20, TimeUnit.SECONDS)
.untilAsserted(() -> {
partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
@@ -481,7 +475,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testDelete_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Set partitions = createTpis(1, 2);
consumerManager.update(partitions);
TestConsumer consumer = getConsumer();
@@ -499,7 +492,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
int msgCount = totalConsumedMsgs.get();
await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call)
- .atMost(7, TimeUnit.SECONDS)
+ .atMost(20, TimeUnit.SECONDS)
.untilAsserted(() -> {
partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
@@ -520,10 +513,9 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testManyDifferentUpdates() throws Exception {
queue.setConsumerPerPartition(RandomUtils.nextBoolean());
consumerManager.init(queue);
- ruleEngineConsumerContext.setReady(true);
Supplier queueConfigUpdater = () -> {
- Queue oldConfig = consumerManager.getQueue();
+ Queue oldConfig = consumerManager.getConfig();
Queue newConfig = JacksonUtil.clone(oldConfig);
newConfig.setConsumerPerPartition(RandomUtils.nextBoolean());
newConfig.setPollInterval(RandomUtils.nextInt(100, 501));
@@ -571,7 +563,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
Set expectedPartitions = latestPartitions;
await().atMost(5, TimeUnit.SECONDS)
.untilAsserted(() -> {
- assertThat(consumerManager.getQueue()).isEqualTo(expectedConfig);
+ assertThat(consumerManager.getConfig()).isEqualTo(expectedConfig);
assertThat(consumerManager.getPartitions()).isEqualTo(expectedPartitions);
});
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java
index 36179502ad..7ab5ee3b56 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java
@@ -39,4 +39,5 @@ public class OtherConfiguration extends PowerSavingConfiguration {
private Long pagingTransmissionWindow;
private String fwUpdateResource;
private String swUpdateResource;
+ private String defaultObjectIDVer;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java
new file mode 100644
index 0000000000..27be0f65d2
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java
@@ -0,0 +1,30 @@
+/**
+ * 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.exception;
+
+import org.thingsboard.server.common.data.limit.LimitedApi;
+
+public class RateLimitExceededException extends AbstractRateLimitException {
+
+ public RateLimitExceededException(String message) {
+ super(message);
+ }
+
+ public RateLimitExceededException(LimitedApi api) {
+ super("Rate limit for " + api.getLabel() + " is exceeded");
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
index 1a0f038c90..4afead3435 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
@@ -39,7 +39,8 @@ public enum LimitedApi {
TWO_FA_VERIFICATION_CODE_SEND(false, true),
TWO_FA_VERIFICATION_CODE_CHECK(false, true),
TRANSPORT_MESSAGES_PER_TENANT("transport messages", true),
- TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false);
+ TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false),
+ EMAILS("emails sending", true);
private Function configExtractor;
@Getter
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java
index e3c6836aab..1c08072e61 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java
@@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Optional;
@Data
-public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId {
+public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, QueueConfig {
private TenantId tenantId;
@NoXss
@Length(fieldName = "name")
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java
new file mode 100644
index 0000000000..383c613a38
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.queue;
+
+public interface QueueConfig {
+
+ boolean isConsumerPerPartition();
+
+ int getPollInterval();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java
index c823ea4d73..f3a0e47d09 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java
@@ -15,11 +15,23 @@
*/
package org.thingsboard.server.common.msg.queue;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
public enum ServiceType {
- TB_CORE, TB_RULE_ENGINE, TB_TRANSPORT, JS_EXECUTOR, TB_VC_EXECUTOR;
+ TB_CORE("TB Core"),
+ TB_RULE_ENGINE("TB Rule Engine"),
+ TB_TRANSPORT("TB Transport"),
+ JS_EXECUTOR("JS Executor"),
+ TB_VC_EXECUTOR("TB VC Executor");
+
+ private final String label;
public static ServiceType of(String serviceType) {
return ServiceType.valueOf(serviceType.replace("-", "_").toUpperCase());
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java
similarity index 98%
rename from application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java
rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java
index e7b3a2f2b7..90f4cf050c 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.queue.consumer;
+package org.thingsboard.server.queue.common.consumer;
import lombok.Builder;
import lombok.Getter;
@@ -106,4 +106,5 @@ public class QueueConsumerManager {
public interface MsgPackProcessor {
void process(List msgs, TbQueueConsumer consumer) throws Exception;
}
-}
\ No newline at end of file
+
+}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
index cd09f682c6..d8efad2182 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
@@ -217,7 +217,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic()));
- consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId());
+ consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-core-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(coreAdmin);
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
index 95fea30d7b..ddb2be8f2a 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
@@ -56,6 +56,7 @@ import org.thingsboard.server.queue.settings.TbQueueVersionControlSettings;
import jakarta.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicLong;
@Component
@ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='tb-core'")
@@ -84,6 +85,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
private final TbQueueAdmin housekeeperAdmin;
private final TbQueueAdmin housekeeperReprocessingAdmin;
+ private final AtomicLong consumerCount = new AtomicLong();
+
public KafkaTbCoreQueueFactory(TopicService topicService,
TbKafkaSettings kafkaSettings,
TbServiceInfoProvider serviceInfoProvider,
@@ -174,7 +177,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic()));
- consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId());
+ consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-core-node"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(coreAdmin);
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
index b59914e2ac..7797242b51 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
@@ -29,6 +29,7 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationListener;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.send.SendListener;
+import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.Collection;
@@ -101,7 +102,10 @@ public class LwM2mServerListener {
@Override
public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) {
if (registration != null) {
- service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), registration), response);
+ LwM2mClient lwM2MClient = service.getClientContext().getClientByEndpoint(registration.getEndpoint());
+ if (lwM2MClient != null) {
+ service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), lwM2MClient), response);
+ }
}
}
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
index 3e407bdfc9..78f76adc1d 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
@@ -27,10 +27,10 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
+import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import java.util.ArrayList;
-import java.util.Base64;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
@@ -107,7 +107,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public ObjectModel getObjectModel(int objectId) {
- String version = String.valueOf(registration.getSupportedVersion(objectId));
+ LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint());
+ String version = lwM2mClient.getSupportedObjectVersion(objectId).toString();
if (version != null) {
return this.getObjectModelDynamic(objectId, version);
}
@@ -116,7 +117,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public Collection getObjectModels() {
- Map supportedObjects = this.registration.getSupportedObject();
+ LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint());
+ Map supportedObjects = lwM2mClient.getSupportedClientObjects();
Collection result = new ArrayList<>(supportedObjects.size());
for (Map.Entry supportedObject : supportedObjects.entrySet()) {
ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), String.valueOf(supportedObject.getValue()));
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
index cb391b27e9..705ed45995 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
@@ -21,6 +21,8 @@ import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m;
+import org.eclipse.leshan.core.LwM2m.Version;
+import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.link.attributes.Attribute;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
@@ -37,6 +39,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.PowerMode;
+import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
@@ -61,8 +64,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
+import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
-import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId;
@@ -129,6 +132,12 @@ public class LwM2mClient {
@Setter
private UUID lastSentRpcId;
+ @Setter
+ private LwM2m.Version defaultObjectIDVer;
+
+ @Getter
+ private Map supportedClientObjects;
+
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@@ -142,6 +151,7 @@ public class LwM2mClient {
this.state = LwM2MClientState.CREATED;
this.lock = new ReentrantLock();
this.retryAttempts = new AtomicInteger(0);
+ this.supportedClientObjects = null;
}
public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) {
@@ -153,12 +163,14 @@ public class LwM2mClient {
this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle();
this.psmActivityTimer = credentials.getDeviceInfo().getPsmActivityTimer();
this.pagingTransmissionWindow = credentials.getDeviceInfo().getPagingTransmissionWindow();
+ this.defaultObjectIDVer = getObjectIDVerFromDeviceProfile(credentials.getDeviceProfile());
}
public void setRegistration(Registration registration) {
this.registration = registration;
this.clientSupportContentFormats = clientSupportContentFormat(registration);
this.defaultContentFormat = calculateDefaultContentFormat(registration);
+ this.setSupportedClientObjects();
}
public void lock() {
@@ -197,6 +209,18 @@ public class LwM2mClient {
builder.setDeviceType(deviceProfile.getName());
}
+ private LwM2m.Version getObjectIDVerFromDeviceProfile(DeviceProfile deviceProfile) {
+ String defaultObjectIdVer = null;
+ if (deviceProfile != null) {
+ defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile
+ .getProfileData()
+ .getTransportConfiguration())
+ .getClientLwM2mSettings()
+ .getDefaultObjectIDVer();
+ }
+ return new Version(defaultObjectIdVer == null ? LWM2M_OBJECT_VERSION_DEFAULT : defaultObjectIdVer);
+ }
+
public void refreshSessionId(String nodeId) {
UUID newId = UUID.randomUUID();
SessionInfoProto.Builder builder = SessionInfoProto.newBuilder().mergeFrom(session);
@@ -240,67 +264,28 @@ public class LwM2mClient {
}
}
- public Object getResourceValue(String pathRezIdVer, String pathRezId) {
- String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
- if (this.resources.get(pathRez) != null) {
- return this.resources.get(pathRez).getLwM2mResource().getValue();
- }
- return null;
- }
-
- public Object getResourceNameByRezId(String pathRezIdVer, String pathRezId) {
- String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
- if (this.resources.get(pathRez) != null) {
- return this.resources.get(pathRez).getResourceModel().name;
- }
- return null;
- }
-
- public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) {
- LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer);
- if (pathIds.isObjectInstance()) {
- Set rezIds = modelProvider.getObjectModel(registration)
- .getObjectModel(pathIds.getObjectId()).resources.entrySet()
- .stream()
- .filter(map -> resourceName.equals(map.getValue().name))
- .map(map -> map.getKey())
- .collect(Collectors.toSet());
- return rezIds.size() > 0 ? String.valueOf(rezIds.stream().findFirst().get()) : null;
- }
- return null;
- }
-
public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer);
- String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
+ String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null;
}
- public boolean isResourceMultiInstances(String pathIdVer, LwM2mModelProvider modelProvider) {
- var resourceModel = getResourceModel(pathIdVer, modelProvider);
- if (resourceModel != null && resourceModel.multiple != null) {
- return resourceModel.multiple;
- } else {
- return false;
- }
- }
-
public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) {
try {
LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer);
- String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
+ String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()) : null;
} catch (Exception e) {
if (registration == null) {
log.error("[{}] Failed Registration is null, GetObjectModelRegistration. ", this.endpoint, e);
- } else if (registration.getSupportedObject() == null) {
+ } else if (this.getSupportedClientObjects() == null) {
log.error("[{}] Failed SupportedObject in Registration, GetObjectModelRegistration.", this.endpoint, e);
} else {
- log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, registration.getSupportedObject(), e);
+ log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, this.getSupportedClientObjects(), e);
}
return null;
}
@@ -371,7 +356,7 @@ public class LwM2mClient {
public String isValidObjectVersion(String path) {
LwM2mPath pathIds = getLwM2mPathFromString(path);
- LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
+ LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId());
if (verSupportedObject == null) {
return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId());
} else {
@@ -460,5 +445,31 @@ public class LwM2mClient {
return new LwM2mPath(fromVersionedIdToObjectId(path));
}
+ public LwM2m.Version getDefaultObjectIDVer() {
+ return this.defaultObjectIDVer == null ? new Version(LWM2M_OBJECT_VERSION_DEFAULT) : this.defaultObjectIDVer;
+ }
+
+ public LwM2m.Version getSupportedObjectVersion(Integer objectid) {
+ return this.supportedClientObjects.get(objectid);
+ }
+
+ private void setSupportedClientObjects(){
+ this.supportedClientObjects = new ConcurrentHashMap<>();
+ for (Link link: this.registration.getSortedObjectLinks()) {
+ LwM2mPath lwM2mPath = new LwM2mPath(link.getUriReference());
+ if (lwM2mPath.isObject()) {
+ LwM2m.Version ver;
+ if (link.getAttributes().get("ver")!= null) {
+ ver = (Version) link.getAttributes().get("ver").getValue();
+ } else {
+ ver = getDefaultObjectIDVer();
+ }
+ this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver);
+ } else if (lwM2mPath.getObjectId() != null && this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){
+ this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer());
+ }
+ }
+ }
+
}
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
index 4f35489250..851b517db6 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
@@ -396,7 +396,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
Arrays.stream(client.getRegistration().getObjectLinks()).forEach(link -> {
LwM2mPath pathIds = new LwM2mPath(link.getUriReference());
if (!pathIds.isRoot()) {
- clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client.getRegistration()));
+ clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client));
}
});
return (clientObjects.size() > 0) ? clientObjects : null;
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
index f9d23383cd..0b5e995ea5 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
@@ -481,7 +481,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void startUpdateUsingUrl(LwM2mClient client, String id, String url) {
- String targetIdVer = convertObjectIdToVersionedId(id, client.getRegistration());
+ String targetIdVer = convertObjectIdToVersionedId(id, client);
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, targetIdVer));
}
@@ -512,10 +512,10 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
switch (strategy) {
case OBJ_5_BINARY:
- startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client.getRegistration()), otaPackageId);
+ startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client), otaPackageId);
break;
case OBJ_19_BINARY:
- startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client.getRegistration()), otaPackageId);
+ startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client), otaPackageId);
break;
case OBJ_5_TEMP_URL:
startUpdateUsingUrl(client, FW_URL_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString());
@@ -534,7 +534,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
LwM2MSoftwareUpdateStrategy strategy = info.getStrategy();
switch (strategy) {
case BINARY:
- startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client.getRegistration()), otaPackageId);
+ startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client), otaPackageId);
break;
case TEMP_URL:
startUpdateUsingUrl(client, SW_PACKAGE_URI_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString());
@@ -566,19 +566,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void executeFwUpdate(LwM2mClient client) {
- String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client.getRegistration());
+ String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId));
}
private void executeSwInstall(LwM2mClient client) {
- String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client.getRegistration());
+ String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId));
}
private void executeSwUninstallForUpdate(LwM2mClient client) {
- String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client.getRegistration());
+ String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId));
}
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
index 17fef8acb0..8cafc2bfd0 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
@@ -23,6 +23,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.protobuf.util.JsonFormat;
import lombok.SneakyThrows;
+import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mNodeException;
@@ -107,6 +108,8 @@ public class LwM2MClientSerDes {
if (client.getPagingTransmissionWindow() != null) {
o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow());
}
+ o.addProperty("defaultObjectIDVer", client.getDefaultObjectIDVer().toString());
+
if (client.getRegistration() != null) {
String registrationAddress = client.getRegistration().getAddress().toString();
JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration());
@@ -339,6 +342,13 @@ public class LwM2MClientSerDes {
pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong());
}
+ JsonElement defaultObjectIDVer = o.get("defaultObjectIDVer");
+ if (defaultObjectIDVer != null) {
+ Field defaultObjectIDVerField = lwM2mClientClass.getDeclaredField("defaultObjectIDVer");
+ defaultObjectIDVerField.setAccessible(true);
+ defaultObjectIDVerField.set(lwM2mClient, new Version(defaultObjectIDVer.getAsString()));
+ }
+
JsonElement registration = o.get("registration");
if (registration != null) {
lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString())));
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
index fc95bb3728..bfc9f32dd8 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
@@ -368,7 +368,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
LwM2mPath path = entry.getKey();
LwM2mNode node = entry.getValue();
LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint());
- String stringPath = convertObjectIdToVersionedId(path.toString(), registration);
+ String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient);
ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider);
if (objectModelVersion != null) {
if (node instanceof LwM2mObject) {
@@ -584,27 +584,27 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path, Mode mode, int code) {
Registration registration = lwM2MClient.getRegistration();
if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) {
- if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, registration))) {
+ if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) {
otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, lwM2MClient))) {
otaService.onCurrentFirmwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, lwM2MClient))) {
otaService.onCurrentFirmwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, lwM2MClient))) {
otaService.onCurrentFirmwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, lwM2MClient))) {
otaService.onCurrentFirmwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, lwM2MClient))) {
otaService.onCurrentFirmwareDeliveryMethodUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, lwM2MClient))) {
otaService.onCurrentSoftwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, lwM2MClient))) {
otaService.onCurrentSoftwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, lwM2MClient))) {
otaService.onCurrentSoftwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, lwM2MClient))) {
otaService.onCurrentSoftwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
- } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, registration))) {
+ } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, lwM2MClient))) {
otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
}
if (ResponseCode.BAD_REQUEST.getCode() > code) {
@@ -969,6 +969,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
}
+ public LwM2mClientContext getClientContext(){
+ return this.clientContext;
+ };
+
private Map getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) {
Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId());
return profile.getObserveAttr().getKeyName();
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java
index 65c7fb9654..0896963be2 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java
@@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
+import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import java.util.Collection;
import java.util.Optional;
@@ -77,4 +78,5 @@ public interface LwM2mUplinkMsgHandler {
LwM2mValueConverter getConverter();
+ LwM2mClientContext getClientContext();
}
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
index db8398cd57..e2e31f2768 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
@@ -20,16 +20,13 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectLoader;
-import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
-import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.util.Hex;
-import org.eclipse.leshan.server.registration.Registration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
@@ -49,8 +46,6 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState;
-import java.util.Arrays;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -84,56 +79,6 @@ public class LwM2MTransportUtil {
public static final String LOG_LWM2M_WARN = "warn";
public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0;
- public enum LwM2MClientStrategy {
- CLIENT_STRATEGY_1(1, "Read only resources marked as observation"),
- CLIENT_STRATEGY_2(2, "Read all client resources");
-
- public int code;
- public String type;
-
- LwM2MClientStrategy(int code, String type) {
- this.code = code;
- this.type = type;
- }
-
- public static LwM2MClientStrategy fromStrategyClientByType(String type) {
- for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) {
- if (to.type.equals(type)) {
- return to;
- }
- }
- throw new IllegalArgumentException(String.format("Unsupported Client Strategy type : %s", type));
- }
-
- public static LwM2MClientStrategy fromStrategyClientByCode(int code) {
- for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) {
- if (to.code == code) {
- return to;
- }
- }
- throw new IllegalArgumentException(String.format("Unsupported Client Strategy code : %s", code));
- }
- }
-
- public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath
- resourcePath) throws CodecException {
- switch (type) {
- case BOOLEAN:
- case INTEGER:
- case FLOAT:
- return String.valueOf(valueOld).equals(String.valueOf(valueNew));
- case TIME:
- return ((Date) valueOld).getTime() == ((Date) valueNew).getTime();
- case STRING:
- case OBJLNK:
- return valueOld.equals(valueNew);
- case OPAQUE:
- return Arrays.equals(Hex.decodeHex(((String) valueOld).toCharArray()), Hex.decodeHex(((String) valueNew).toCharArray()));
- default:
- throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type);
- }
- }
-
public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) {
String path = fromVersionedIdToObjectId(pathIdVer);
LwM2mOtaConvert lwM2mOtaConvert = new LwM2mOtaConvert();
@@ -210,22 +155,8 @@ public class LwM2MTransportUtil {
return null;
}
- public static String validPathIdVer(String pathIdVer, Registration registration) throws
- IllegalArgumentException {
- if (!pathIdVer.contains(LWM2M_SEPARATOR_PATH)) {
- throw new IllegalArgumentException(String.format("Error:"));
- } else {
- String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH);
- if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) {
- return pathIdVer;
- } else {
- return convertObjectIdToVersionedId(pathIdVer, registration);
- }
- }
- }
-
- public static String convertObjectIdToVersionedId(String path, Registration registration) {
- String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()));
+ public static String convertObjectIdToVersionedId(String path, LwM2mClient lwM2MClient) {
+ String ver = String.valueOf(lwM2MClient.getSupportedObjectVersion(new LwM2mPath(path).getObjectId()));
return convertObjectIdToVerId(path, ver);
}
public static String convertObjectIdToVerId(String path, String ver) {
@@ -243,14 +174,6 @@ public class LwM2MTransportUtil {
}
}
- public static String validateObjectVerFromKey(String key) {
- try {
- return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]);
- } catch (Exception e) {
- return ObjectModel.DEFAULT_VERSION;
- }
- }
-
/**
* "UNSIGNED_INTEGER": // Number -> Integer Example:
* Alarm Timestamp [32-bit unsigned integer]
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 41886bd88a..8b7b88c261 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
@@ -21,6 +21,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -97,6 +99,7 @@ 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.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.TopicService;
@@ -106,14 +109,12 @@ import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbTransportComponent;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
-import java.util.Collections;
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.ExecutionException;
@@ -179,19 +180,17 @@ public class DefaultTransportService extends TransportActivityManager implements
protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate;
protected TbQueueProducer> ruleEngineMsgProducer;
protected TbQueueProducer> tbCoreMsgProducer;
- protected TbQueueConsumer> transportNotificationsConsumer;
+ protected QueueConsumerManager> transportNotificationsConsumer;
protected MessagesStats ruleEngineProducerStats;
protected MessagesStats tbCoreProducerStats;
protected MessagesStats transportApiStats;
protected ExecutorService transportCallbackExecutor;
- private ExecutorService mainConsumerExecutor;
+ private ExecutorService consumerExecutor;
private final Map toServerRpcPendingMap = new ConcurrentHashMap<>();
- private volatile boolean stopped = false;
-
public DefaultTransportService(PartitionService partitionService,
TbServiceInfoProvider serviceInfoProvider,
TbTransportQueueFactory queueProvider,
@@ -232,42 +231,33 @@ public class DefaultTransportService extends TransportActivityManager implements
transportApiRequestTemplate.setMessagesStats(transportApiStats);
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer();
- transportNotificationsConsumer = queueProvider.createTransportNotificationsConsumer();
- TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId());
- transportNotificationsConsumer.subscribe(Collections.singleton(tpi));
transportApiRequestTemplate.init();
- mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer"));
+ consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer"));
+ transportNotificationsConsumer = QueueConsumerManager.>builder()
+ .name("TB Transport")
+ .msgPackProcessor(this::processNotificationMsgs)
+ .pollInterval(notificationsPollDuration)
+ .consumerCreator(queueProvider::createTransportNotificationsConsumer)
+ .consumerExecutor(consumerExecutor)
+ .build();
}
@AfterStartUp(order = AfterStartUp.TRANSPORT_SERVICE)
public void start() {
- mainConsumerExecutor.execute(() -> {
- while (!stopped) {
- try {
- List> records = transportNotificationsConsumer.poll(notificationsPollDuration);
- if (records.size() == 0) {
- continue;
- }
- records.forEach(record -> {
- try {
- processToTransportMsg(record.getValue());
- } catch (Throwable e) {
- log.warn("Failed to process the notification.", e);
- }
- });
- transportNotificationsConsumer.commit();
- } catch (Exception e) {
- if (!stopped) {
- log.warn("Failed to obtain messages from queue.", e);
- try {
- Thread.sleep(notificationsPollDuration);
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new requests", e2);
- }
- }
- }
+ TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId());
+ transportNotificationsConsumer.subscribe(Set.of(tpi));
+ transportNotificationsConsumer.launch();
+ }
+
+ private void processNotificationMsgs(List> msgs, TbQueueConsumer> consumer) {
+ msgs.forEach(msg -> {
+ try {
+ processToTransportMsg(msg.getValue());
+ } catch (Throwable e) {
+ log.warn("Failed to process the notification.", e);
}
});
+ consumer.commit();
}
private void invalidateRateLimits() {
@@ -276,16 +266,14 @@ public class DefaultTransportService extends TransportActivityManager implements
@PreDestroy
public void destroy() {
- stopped = true;
-
if (transportNotificationsConsumer != null) {
- transportNotificationsConsumer.unsubscribe();
+ transportNotificationsConsumer.stop();
}
if (transportCallbackExecutor != null) {
transportCallbackExecutor.shutdownNow();
}
- if (mainConsumerExecutor != null) {
- mainConsumerExecutor.shutdownNow();
+ if (consumerExecutor != null) {
+ consumerExecutor.shutdownNow();
}
if (transportApiRequestTemplate != null) {
transportApiRequestTemplate.stop();
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 edf998dc8e..34171c19cd 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
@@ -21,14 +21,13 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.jgit.errors.LargeObjectException;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.context.event.ApplicationReadyEvent;
-import org.springframework.context.event.EventListener;
-import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.EntityType;
@@ -68,16 +67,16 @@ import org.thingsboard.server.gen.transport.TransportProtos.VersionedEntityInfoP
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbVersionControlQueueFactory;
+import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbVersionControlComponent;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
@@ -115,9 +114,8 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
private final Map pendingCommitMap = new HashMap<>();
private volatile ExecutorService consumerExecutor;
- private volatile TbQueueConsumer> consumer;
+ private volatile QueueConsumerManager> consumer;
private volatile TbQueueProducer> producer;
- private volatile boolean stopped = false;
@Value("${queue.vc.poll-interval:25}")
private long pollDuration;
@@ -134,20 +132,25 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
@PostConstruct
public void init() {
- consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("vc-consumer"));
+ consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("vc-consumer"));
var threadFactory = ThingsBoardThreadFactory.forName("vc-io-thread");
for (int i = 0; i < ioPoolSize; i++) {
ioThreads.add(MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(threadFactory)));
}
producer = producerProvider.getTbCoreNotificationsMsgProducer();
- consumer = queueFactory.createToVersionControlMsgConsumer();
+ consumer = QueueConsumerManager.>builder()
+ .name("TB Version Control")
+ .msgPackProcessor(this::processMsgs)
+ .pollInterval(pollDuration)
+ .consumerCreator(queueFactory::createToVersionControlMsgConsumer)
+ .consumerExecutor(consumerExecutor)
+ .build();
}
@PreDestroy
public void stop() {
- stopped = true;
if (consumer != null) {
- consumer.unsubscribe();
+ consumer.stop();
}
if (consumerExecutor != null) {
consumerExecutor.shutdownNow();
@@ -179,48 +182,29 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
return ServiceType.TB_VC_EXECUTOR.equals(event.getServiceType());
}
- @EventListener(ApplicationReadyEvent.class)
- @Order(value = 2)
- public void onApplicationEvent(ApplicationReadyEvent event) {
- consumerExecutor.execute(() -> consumerLoop(consumer));
+ @AfterStartUp(order = 2)
+ public void afterStartUp() {
+ consumer.launch();
}
- void consumerLoop(TbQueueConsumer> consumer) {
- while (!stopped && !consumer.isStopped()) {
- List> futures = new ArrayList<>();
- try {
- List> msgs = consumer.poll(pollDuration);
- if (msgs.isEmpty()) {
- continue;
- }
- for (TbProtoQueueMsg msgWrapper : msgs) {
- ToVersionControlServiceMsg msg = msgWrapper.getValue();
- var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings()));
- long startTs = System.currentTimeMillis();
- log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg);
- int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize);
- ListenableFuture future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg));
- logTaskExecution(ctx, future, startTs);
- futures.add(future);
- }
- try {
- Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS);
- } catch (TimeoutException e) {
- log.info("Timeout for processing the version control tasks.", e);
- }
- consumer.commit();
- } catch (Exception e) {
- if (!stopped) {
- log.warn("Failed to obtain version control requests from queue.", e);
- try {
- Thread.sleep(pollDuration);
- } catch (InterruptedException e2) {
- log.trace("Failed to wait until the server has capacity to handle new version control messages", e2);
- }
- }
- }
+ void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception {
+ List> futures = new ArrayList<>();
+ for (TbProtoQueueMsg msgWrapper : msgs) {
+ ToVersionControlServiceMsg msg = msgWrapper.getValue();
+ var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings()));
+ long startTs = System.currentTimeMillis();
+ log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg);
+ int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize);
+ ListenableFuture future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg));
+ logTaskExecution(ctx, future, startTs);
+ futures.add(future);
+ }
+ try {
+ Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException e) {
+ log.info("Timeout for processing the version control tasks.", e);
}
- log.info("TB Version Control request consumer stopped.");
+ consumer.commit();
}
private Void processMessage(VersionControlRequestCtx ctx, ToVersionControlServiceMsg msg) {
@@ -273,7 +257,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
var ids = vcService.listEntitiesAtVersion(ctx.getTenantId(), request.getVersionId(), path)
.stream().skip(request.getOffset()).limit(request.getLimit()).collect(Collectors.toList());
if (!ids.isEmpty()) {
- for (int i = 0; i < ids.size(); i++){
+ for (int i = 0; i < ids.size(); i++) {
VersionedEntityInfo info = ids.get(i);
var data = vcService.getFileContentAtCommit(ctx.getTenantId(),
getRelativePath(info.getExternalId().getEntityType(), info.getExternalId().getId().toString()), request.getVersionId());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
index f65e0db1c6..bf4798fa4d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
@@ -41,7 +41,9 @@ public interface AssetRepository extends JpaRepository, Expor
AssetInfoEntity findAssetInfoById(@Param("assetId") UUID assetId);
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
- "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
+ "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)")
Page findByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@@ -54,14 +56,16 @@ public interface AssetRepository extends JpaRepository, Expor
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(p.name, CONCAT('%', :textSearch, '%')) = true " +
- " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ")
+ " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true)")
Page findAssetInfosByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
- "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
+ "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)")
Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("textSearch") String textSearch,
@@ -69,7 +73,8 @@ public interface AssetRepository extends JpaRepository, Expor
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.assetProfileId = :profileId " +
- "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
+ "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true)")
Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId,
@Param("profileId") UUID profileId,
@Param("searchText") String searchText,
@@ -81,7 +86,10 @@ public interface AssetRepository extends JpaRepository, Expor
"LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " +
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
- "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
+ "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(c.title, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(p.name, CONCAT('%', :searchText, '%')) = true) ")
Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("searchText") String searchText,
@@ -95,7 +103,8 @@ public interface AssetRepository extends JpaRepository, Expor
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.type = :type " +
- "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
+ "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true)")
Page findByTenantIdAndType(@Param("tenantId") UUID tenantId,
@Param("type") String type,
@Param("textSearch") String textSearch,
@@ -123,7 +132,8 @@ public interface AssetRepository extends JpaRepository, Expor
"AND a.assetProfileId = :assetProfileId " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
" OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
- " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ")
+ " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true) ")
Page findAssetInfosByTenantIdAndAssetProfileId(@Param("tenantId") UUID tenantId,
@Param("assetProfileId") UUID assetProfileId,
@Param("textSearch") String textSearch,
@@ -132,7 +142,8 @@ public interface AssetRepository extends JpaRepository, Expor
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId AND a.type = :type " +
- "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
+ "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ")
Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("type") String type,
@@ -146,7 +157,9 @@ public interface AssetRepository extends JpaRepository, Expor
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND a.type = :type " +
- "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
+ "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ")
Page findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("type") String type,
@@ -160,7 +173,10 @@ public interface AssetRepository extends JpaRepository, Expor
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND a.assetProfileId = :assetProfileId " +
- "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
+ "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true " +
+ " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true) ")
Page findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("assetProfileId") UUID assetProfileId,
@@ -172,7 +188,9 @@ public interface AssetRepository extends JpaRepository, Expor
@Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " +
"AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " +
"AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " +
- "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
+ "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(a.type, CONCAT('%', :searchText, '%')) = true) ")
Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId,
@Param("edgeId") UUID edgeId,
@Param("searchText") String searchText,
@@ -182,7 +200,8 @@ public interface AssetRepository extends JpaRepository, Expor
"AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " +
"AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " +
"AND a.type = :type " +
- "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true)")
+ "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " +
+ " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ")
Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId,
@Param("edgeId") UUID edgeId,
@Param("type") String type,
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 073dfd7d95..ddd6c3c881 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
@@ -45,7 +45,7 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByDeviceProfileId(@Param("tenantId") UUID tenantId,
- @Param("deviceProfileId") UUID deviceProfileId);
+ @Param("deviceProfileId") UUID deviceProfileId);
@Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE " +
"entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true)
@@ -58,6 +58,6 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds,
- @Param("attributeType") String attributeType);
+ @Param("attributeType") int 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 3bf8e43c70..320e0e67fb 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
@@ -173,7 +173,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
@Override
public List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType) {
return attributeKvRepository
- .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), attributeType)
+ .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId())
.stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList());
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
index 707155c86c..98a8cf9d47 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
@@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
+import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -47,6 +48,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
+import static org.assertj.core.api.Assertions.assertThat;
+
/**
* @author Andrew Shvayka
*/
@@ -221,6 +224,45 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
Assert.assertEquals(NEW_VALUE, value.get(1));
}
+ @Test
+ public void testFindAllKeysByEntityId() {
+ var tenantId = new TenantId(UUID.randomUUID());
+ var deviceId = new DeviceId(UUID.randomUUID());
+ saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123");
+ saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
+
+ Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
+ List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId));
+ assertThat(keys).containsOnly("key1", "key2");
+ });
+ }
+
+ @Test
+ public void testFindAllKeysByEntityIdAndAttributeType() {
+ var tenantId = new TenantId(UUID.randomUUID());
+ var deviceId = new DeviceId(UUID.randomUUID());
+ saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123");
+ saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
+
+ Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
+ List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name());
+ assertThat(keys).containsOnly("key1", "key2");
+ });
+ }
+
+ @Test
+ public void testFindAllByEntityIdAndAttributeType() {
+ var tenantId = new TenantId(UUID.randomUUID());
+ var deviceId = new DeviceId(UUID.randomUUID());
+ saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123");
+ saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
+
+ Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
+ List attributes = attributesService.findAll(tenantId, deviceId, AttributeScope.SERVER_SCOPE).get();
+ assertThat(attributes).extracting(KvEntry::getKey).containsOnly("key1", "key2");
+ });
+ }
+
private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception {
var scope = AttributeScope.SERVER_SCOPE;
var key = "TEST";
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
index bd6669f471..3e4b7ca38a 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java
@@ -78,7 +78,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
UUID assetId = Uuids.timeBased();
UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2;
UUID customerId = i % 2 == 0 ? customerId1 : customerId2;
- assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i));
+ assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "label_" + i));
}
assertEquals(assets.size(), assetDao.find(TenantId.fromUUID(tenantId1)).size());
}
@@ -131,6 +131,21 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
assertEquals(0, assets3.getData().size());
}
+ @Test
+ public void testFindAssetsByTenantIdAndCustomerIdByLabel() {
+ PageLink pageLink = new PageLink(20, 0, "label_");
+ PageData assets1 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink);
+ assertEquals(20, assets1.getData().size());
+
+ pageLink = pageLink.nextPageLink();
+ PageData assets2 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink);
+ assertEquals(10, assets2.getData().size());
+
+ pageLink = pageLink.nextPageLink();
+ PageData assets3 = assetDao.findAssetsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink);
+ assertEquals(0, assets3.getData().size());
+ }
+
@Test
public void testFindAssetsByTenantIdAndIdsAsync() throws ExecutionException, InterruptedException, TimeoutException {
List searchIds = getAssetsUuids(tenantId1);
@@ -180,21 +195,31 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
@Test
public void testFindAssetsByTenantIdAndType() {
String type = "TYPE_2";
- assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type));
+ String testLabel = "test_label";
+ assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type, testLabel));
List foundedAssetsByType = assetDao
.findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3)).getData();
compareFoundedAssetByType(foundedAssetsByType, type);
+
+ List foundedAssetsByTypeAndLabel = assetDao
+ .findAssetsByTenantIdAndType(tenantId2, type, new PageLink(3, 0, testLabel)).getData();
+ assertEquals(1, foundedAssetsByTypeAndLabel.size());
}
@Test
public void testFindAssetsByTenantIdAndCustomerIdAndType() {
String type = "TYPE_2";
- assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type));
+ String testLabel = "test_label";
+ assets.add(saveAsset(Uuids.timeBased(), tenantId2, customerId2, "TEST_ASSET", type, testLabel));
List foundedAssetsByType = assetDao
.findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3)).getData();
compareFoundedAssetByType(foundedAssetsByType, type);
+
+ List foundedAssetsByTypeAndLabel = assetDao
+ .findAssetsByTenantIdAndCustomerIdAndType(tenantId2, customerId2, type, new PageLink(3, 0, testLabel)).getData();
+ assertEquals(1, foundedAssetsByTypeAndLabel.size());
}
private void compareFoundedAssetByType(List foundedAssetsByType, String type) {
@@ -228,10 +253,14 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
}
private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name) {
- return saveAsset(id, tenantId, customerId, name, null);
+ return saveAsset(id, tenantId, customerId, name, null, null);
+ }
+
+ private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String label) {
+ return saveAsset(id, tenantId, customerId, name, null, label);
}
- private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type) {
+ private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type, String label) {
if (type == null) {
type = "default";
}
@@ -241,6 +270,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest {
asset.setCustomerId(new CustomerId(customerId));
asset.setName(name);
asset.setType(type);
+ asset.setLabel(label);
asset.setAssetProfileId(assetProfileId(type));
return assetDao.save(TenantId.fromUUID(tenantId), asset);
}
diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json
index cc78187938..c75fe9ce7a 100644
--- a/monitoring/src/main/resources/lwm2m/device_profile.json
+++ b/monitoring/src/main/resources/lwm2m/device_profile.json
@@ -12,14 +12,14 @@
"transportConfiguration": {
"observeAttr": {
"observe": [
- "/3_1.1/0/0"
+ "/3_1.0/0/0"
],
"attribute": [],
"telemetry": [
- "/3_1.1/0/0"
+ "/3_1.0/0/0"
],
"keyName": {
- "/3_1.1/0/0": "testData"
+ "/3_1.0/0/0": "testData"
},
"attributeLwm2m": {}
},
@@ -44,8 +44,7 @@
"clientOnlyObserveAfterConnect": 1,
"fwUpdateStrategy": 1,
"swUpdateStrategy": 1,
- "powerMode": "DRX",
- "compositeOperationsSupport": false
+ "powerMode": "DRX"
},
"bootstrapServerUpdateEnable": false,
"type": "LWM2M"
diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml
index b13934531d..7b71b5e783 100644
--- a/monitoring/src/main/resources/lwm2m/models/test-model.xml
+++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml
@@ -23,9 +23,9 @@
3
- urn:oma:lwm2m:oma:3:1.1
+ urn:oma:lwm2m:oma:3
1.1
- 1.1
+ 1.0
Single
Mandatory
diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json
index 3f4ec5c3fe..dcc0182bdd 100644
--- a/monitoring/src/main/resources/lwm2m/resource.json
+++ b/monitoring/src/main/resources/lwm2m/resource.json
@@ -2,5 +2,5 @@
"title": "",
"resourceType": "LWM2M_MODEL",
"fileName": "test-model.xml",
- "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMTwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4xPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K"
+ "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjQgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozPC9PYmplY3RVUk4+CiAgICAgICAgPExXTTJNVmVyc2lvbj4xLjE8L0xXTTJNVmVyc2lvbj4KICAgICAgICA8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CiAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgPE1hbmRhdG9yeT5NYW5kYXRvcnk8L01hbmRhdG9yeT4KICAgICAgICA8UmVzb3VyY2VzPgogICAgICAgICAgICA8SXRlbSBJRD0iMCI+CiAgICAgICAgICAgICAgICA8TmFtZT5UZXN0IGRhdGE8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUZXN0IGRhdGFdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgPC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo="
}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c06166e497..c3451544a4 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1476,6 +1476,12 @@
org.apache.httpcomponents
httpclient
${apache-httpclient.version}
+
+
+ commons-logging
+ commons-logging
+
+
org.apache.httpcomponents
@@ -1943,6 +1949,12 @@
org.elasticsearch.client
elasticsearch-rest-client
${elasticsearch.version}
+
+
+ commons-logging
+ commons-logging
+
+
org.javadelight
@@ -1968,11 +1980,23 @@
com.amazonaws
aws-java-sdk-sqs
${aws.sdk.version}
+
+
+ commons-logging
+ commons-logging
+
+
com.amazonaws
aws-java-sdk-sns
${aws.sdk.version}
+
+
+ commons-logging
+ commons-logging
+
+
com.google.cloud
@@ -2102,6 +2126,10 @@
org.slf4j
*
+
+ commons-logging
+ commons-logging
+
com.github.spotbugs
spotbugs-annotations
@@ -2136,6 +2164,12 @@
mockserver-netty
${mock-server.version}
test
+
+
+ commons-logging
+ commons-logging
+
+
org.mock-server
@@ -2168,6 +2202,12 @@
com.google.firebase
firebase-admin
${firebase-admin.version}
+
+
+ commons-logging
+ commons-logging
+
+
org.eclipse.jgit
@@ -2203,6 +2243,12 @@
org.apache.xmlgraphics
batik-transcoder
${apache-xmlgraphics.version}
+
+
+ commons-logging
+ commons-logging
+
+
org.apache.xmlgraphics
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java
index 4baec1c882..0b30d02842 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java
@@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.flow;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@@ -22,10 +24,14 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
+import java.util.Optional;
import java.util.UUID;
@Slf4j
@@ -33,11 +39,13 @@ import java.util.UUID;
type = ComponentType.FLOW,
name = "rule chain",
configClazz = TbRuleChainInputNodeConfiguration.class,
- nodeDescription = "transfers the message to another rule chain",
- nodeDetails = "Allows to nest the rule chain similar to single rule node. " +
- "The incoming message is forwarded to the input node of the specified target rule chain. " +
- "The target rule chain may produce multiple labeled outputs. " +
- "You may use the outputs to forward the results of processing to other rule nodes.",
+ version = 1,
+ nodeDescription = "Transfers the message to another rule chain",
+ nodeDetails = "The incoming message is forwarded to the input node of target rule chain. " +
+ "If 'Forward message to the originator's default rule chain' is enabled, " +
+ "then target rule chain might be resolved dynamically based on incoming message originator. " +
+ "In this case rule chain specified in the configuration will be used as fallback rule chain.
" +
+ "Output connections: Any connection(s) produced by output node(s) in the target rule chain.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFlowNodeRuleChainInputConfig",
relationTypes = {},
@@ -46,19 +54,57 @@ import java.util.UUID;
)
public class TbRuleChainInputNode implements TbNode {
- private TbRuleChainInputNodeConfiguration config;
private RuleChainId ruleChainId;
+ private boolean forwardMsgToDefaultRuleChain;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
- this.config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class);
- this.ruleChainId = new RuleChainId(UUID.fromString(config.getRuleChainId()));
+ TbRuleChainInputNodeConfiguration config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class);
+ if (config.getRuleChainId() == null) {
+ throw new TbNodeException("Rule chain must be set!", true);
+ }
+ UUID ruleChainUUID;
+ try {
+ ruleChainUUID = UUID.fromString(config.getRuleChainId());
+ } catch (Exception e) {
+ throw new TbNodeException("Failed to parse rule chain id: " + config.getRuleChainId(), true);
+ }
+ ruleChainId = new RuleChainId(ruleChainUUID);
ctx.checkTenantEntity(ruleChainId);
+ forwardMsgToDefaultRuleChain = config.isForwardMsgToDefaultRuleChain();
}
@Override
- public void onMsg(TbContext ctx, TbMsg msg) {
- ctx.input(msg, ruleChainId);
+ public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
+ RuleChainId targetRuleChainId = forwardMsgToDefaultRuleChain ?
+ getOriginatorDefaultRuleChainId(ctx, msg).orElse(ruleChainId) : ruleChainId;
+ ctx.input(msg, targetRuleChainId);
}
+ @Override
+ public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
+ boolean hasChanges = false;
+ switch (fromVersion) {
+ case 0 -> {
+ if (!oldConfiguration.has("forwardMsgToDefaultRuleChain")) {
+ hasChanges = true;
+ ((ObjectNode) oldConfiguration).put("forwardMsgToDefaultRuleChain", false);
+ }
+ }
+ default -> {
+ }
+ }
+ return new TbPair<>(hasChanges, oldConfiguration);
+ }
+
+ private Optional getOriginatorDefaultRuleChainId(TbContext ctx, TbMsg msg) {
+ return Optional.ofNullable(
+ switch (msg.getOriginator().getEntityType()) {
+ case DEVICE ->
+ ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceId) msg.getOriginator()).getDefaultRuleChainId();
+ case ASSET ->
+ ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetId) msg.getOriginator()).getDefaultRuleChainId();
+ default -> null;
+ });
+ }
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java
index 30c2a49821..84ddb779f7 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java
@@ -22,10 +22,13 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
public class TbRuleChainInputNodeConfiguration implements NodeConfiguration {
private String ruleChainId;
+ private boolean forwardMsgToDefaultRuleChain;
@Override
public TbRuleChainInputNodeConfiguration defaultConfiguration() {
- return new TbRuleChainInputNodeConfiguration();
+ TbRuleChainInputNodeConfiguration configuration = new TbRuleChainInputNodeConfiguration();
+ configuration.setForwardMsgToDefaultRuleChain(false);
+ return configuration;
}
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
index 281fdcb16a..f03d1e46ba 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
@@ -244,27 +244,27 @@ class AlarmState {
return null;
}
} else {
- currentAlarm = new Alarm();
- currentAlarm.setType(alarmDefinition.getAlarmType());
- currentAlarm.setAcknowledged(false);
- currentAlarm.setCleared(false);
- currentAlarm.setSeverity(severity);
+ var newAlarm = new Alarm();
+ newAlarm.setType(alarmDefinition.getAlarmType());
+ newAlarm.setAcknowledged(false);
+ newAlarm.setCleared(false);
+ newAlarm.setSeverity(severity);
long startTs = dataSnapshot.getTs();
if (startTs == 0L) {
startTs = System.currentTimeMillis();
}
- currentAlarm.setStartTs(startTs);
- currentAlarm.setEndTs(currentAlarm.getStartTs());
- currentAlarm.setDetails(createDetails(ruleState));
- currentAlarm.setOriginator(originator);
- currentAlarm.setTenantId(ctx.getTenantId());
- currentAlarm.setPropagate(alarmDefinition.isPropagate());
- currentAlarm.setPropagateToOwner(alarmDefinition.isPropagateToOwner());
- currentAlarm.setPropagateToTenant(alarmDefinition.isPropagateToTenant());
+ newAlarm.setStartTs(startTs);
+ newAlarm.setEndTs(startTs);
+ newAlarm.setDetails(createDetails(ruleState));
+ newAlarm.setOriginator(originator);
+ newAlarm.setTenantId(ctx.getTenantId());
+ newAlarm.setPropagate(alarmDefinition.isPropagate());
+ newAlarm.setPropagateToOwner(alarmDefinition.isPropagateToOwner());
+ newAlarm.setPropagateToTenant(alarmDefinition.isPropagateToTenant());
if (alarmDefinition.getPropagateRelationTypes() != null) {
- currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes());
+ newAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes());
}
- AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(currentAlarm));
+ AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java
new file mode 100644
index 0000000000..579ed7f035
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java
@@ -0,0 +1,299 @@
+/**
+ * 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.rule.engine.flow;
+
+import org.assertj.core.api.Assertions;
+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.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
+import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.asset.AssetProfile;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.msg.TbMsgType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class TbRuleChainInputNodeTest extends AbstractRuleNodeUpgradeTest {
+
+ private final TenantId TENANT_ID = new TenantId(UUID.fromString("4ba69ea5-6b27-42df-ab66-e7a727a67027"));
+ private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("97731954-2147-4176-8f1a-d14f1b73e4e6"));
+ private final AssetId ASSET_ID = new AssetId(UUID.fromString("841a47bd-4e8e-4ea5-88e6-420da0d70e51"));
+
+ private TbRuleChainInputNode node;
+ private TbRuleChainInputNodeConfiguration config;
+ private TbNodeConfiguration nodeConfiguration;
+
+ @Mock
+ private TbContext ctxMock;
+ @Mock
+ private RuleEngineDeviceProfileCache deviceProfileCacheMock;
+ @Mock
+ private RuleEngineAssetProfileCache assetProfileCacheMock;
+
+ @BeforeEach
+ public void setUp() {
+ node = spy(new TbRuleChainInputNode());
+ config = new TbRuleChainInputNodeConfiguration().defaultConfiguration();
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void givenValidConfig_whenInit_thenOk(String ruleChainIdStr, boolean forwardMsgToDefaultRuleChain) throws TbNodeException {
+ //GIVEN
+ config.setRuleChainId(ruleChainIdStr);
+ config.setForwardMsgToDefaultRuleChain(forwardMsgToDefaultRuleChain);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ //WHEN
+ assertThatCode(() -> node.init(ctxMock, nodeConfiguration))
+ .doesNotThrowAnyException();
+
+ //THEN
+ verify(ctxMock).checkTenantEntity(new RuleChainId(UUID.fromString(ruleChainIdStr)));
+ }
+
+ private static Stream givenValidConfig_whenInit_thenOk() {
+ return Stream.of(
+ Arguments.of("45bba7c4-04bf-419b-ae03-6ceb9724f10e", false),
+ Arguments.of("52d57e1b-70bb-480e-bcc4-6710e1dcc9d8", true)
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"91acbce0-079fdb", "", " ", "my test string"})
+ public void givenInvalidRuleChainId_whenInit_thenThrowsException(String ruleChainIdStr) {
+ //GIVEN
+ config.setRuleChainId(ruleChainIdStr);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ //WHEN-THEN
+ Assertions.assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration))
+ .isInstanceOf(TbNodeException.class)
+ .hasMessage("Failed to parse rule chain id: " + ruleChainIdStr);
+ }
+
+ @Test
+ public void givenRuleChainIdIsNotSet_whenInit_thenThrowsException() {
+ assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration))
+ .isInstanceOf(TbNodeException.class)
+ .hasMessage("Rule chain must be set!");
+ }
+
+ @Test
+ public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToDeviceDefaultRuleChain() throws TbNodeException {
+ //GIVEN
+ DeviceProfile deviceProfile = new DeviceProfile();
+ RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("196e3cd5-68b8-421e-a0cf-1d44fa377cdf"));
+ deviceProfile.setDefaultRuleChainId(defaultRuleChainId);
+
+ TbMsg msg = getMsg(DEVICE_ID);
+
+ String ruleChainIdFromConfigStr = "acbc924f-7f95-4a9b-a854-e4822deb74c7";
+ config.setRuleChainId(ruleChainIdFromConfigStr);
+ config.setForwardMsgToDefaultRuleChain(true);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock);
+ when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile);
+
+ node.init(ctxMock, nodeConfiguration);
+
+ //WHEN
+ node.onMsg(ctxMock, msg);
+
+ //THEN
+ ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class);
+ verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture());
+ RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue();
+ assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId);
+
+ RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId");
+ assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)));
+ }
+
+ @Test
+ public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToAssetDefaultRuleChain() throws TbNodeException {
+ //GIVEN
+ AssetProfile assetProfile = new AssetProfile();
+ RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("f0a3cd58-980c-4730-a40c-8f59064d2065"));
+ assetProfile.setDefaultRuleChainId(defaultRuleChainId);
+
+ TbMsg msg = getMsg(ASSET_ID);
+
+ String ruleChainIdFromConfigStr = "56f1c0b8-1a00-4ce0-b3ab-a1416d7cc429";
+ config.setRuleChainId(ruleChainIdFromConfigStr);
+ config.setForwardMsgToDefaultRuleChain(true);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock);
+ when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile);
+
+ node.init(ctxMock, nodeConfiguration);
+
+ //WHEN
+ node.onMsg(ctxMock, msg);
+
+ //THEN
+ ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class);
+ verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture());
+ RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue();
+ assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId);
+
+ RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId");
+ assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)));
+ }
+
+ @Test
+ public void givenForwardMsgToDefaultIsTrueWithoutDeviceDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException {
+ //GIVEN
+ DeviceProfile deviceProfile = new DeviceProfile();
+
+ TbMsg msg = getMsg(DEVICE_ID);
+
+ String ruleChainIdFromConfigStr = "357c2785-e7cc-46a8-9797-957180dabdeb";
+ RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr));
+ config.setRuleChainId(ruleChainIdFromConfigStr);
+ config.setForwardMsgToDefaultRuleChain(true);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock);
+ when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile);
+
+ node.init(ctxMock, nodeConfiguration);
+
+ //WHEN
+ node.onMsg(ctxMock, msg);
+
+ //THEN
+ ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class);
+ verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture());
+ assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig);
+ }
+
+ @Test
+ public void givenForwardMsgToDefaultIsTrueWithoutAssetDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException {
+ //GIVEN
+ AssetProfile assetProfile = new AssetProfile();
+
+ TbMsg msg = getMsg(ASSET_ID);
+
+ String ruleChainIdFromConfigStr = "12883c3d-c10b-4d5b-b606-a59385a920bc";
+ RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr));
+ config.setRuleChainId(ruleChainIdFromConfigStr);
+ config.setForwardMsgToDefaultRuleChain(true);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock);
+ when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile);
+
+ node.init(ctxMock, nodeConfiguration);
+
+ //WHEN
+ node.onMsg(ctxMock, msg);
+
+ //THEN
+ ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class);
+ verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture());
+ assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig);
+ }
+
+ @Test
+ public void givenRuleChainInConfig_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException {
+ //GIVEN
+ String ruleChainIdFromConfigStr = "3c02c8b3-645c-4e67-aac5-f984f59471d1";
+ RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr));
+
+ TbMsg msg = getMsg(DEVICE_ID);
+
+ config.setRuleChainId(ruleChainIdFromConfigStr);
+ config.setForwardMsgToDefaultRuleChain(false);
+ nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
+
+ node.init(ctxMock, nodeConfiguration);
+
+ //WHEN
+ node.onMsg(ctxMock, msg);
+
+ //THEN
+ ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class);
+ verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture());
+ assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig);
+ }
+
+ private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
+ return Stream.of(
+ //config for version 0
+ Arguments.of(0,
+ "{\"ruleChainId\": null}",
+ true,
+ "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}"
+ ),
+ //config for version 1 with upgrade from version 0
+ Arguments.of(1,
+ "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}",
+ false,
+ "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}"
+ )
+ );
+ }
+
+ @Override
+ protected TbNode getTestNode() {
+ return node;
+ }
+
+ private TbMsg getMsg(EntityId entityId) {
+ return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
+ }
+}
diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts
index 5bce9115e8..d437bd73d0 100644
--- a/ui-ngx/src/app/core/api/data-aggregator.ts
+++ b/ui-ngx/src/app/core/api/data-aggregator.ts
@@ -48,7 +48,8 @@ class AggDataMap {
constructor(
private subsTw: SubscriptionTimewindow,
- private endTs: number
+ private endTs: number,
+ private aggType: AggregationType
){};
set(ts: number, data: AggData) {
@@ -78,7 +79,7 @@ class AggDataMap {
}
calculateAggInterval(timestamp: number): [number, number] {
- return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp);
+ return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp, this.aggType);
}
updateLastInterval(endTs: number) {
@@ -87,7 +88,7 @@ class AggDataMap {
const lastTs = this.map.maxKey();
if (lastTs) {
const data = this.map.get(lastTs);
- const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts);
+ const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts, this.aggType);
data.interval = interval;
data.ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2);
}
@@ -415,7 +416,7 @@ export class DataAggregator {
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = aggregationMap.aggMap[id];
if (!aggKeyData) {
- aggKeyData = new AggDataMap(this.subsTw, this.endTs);
+ aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType);
aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
@@ -449,7 +450,7 @@ export class DataAggregator {
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = this.aggregationMap.aggMap[id];
if (!aggKeyData) {
- aggKeyData = new AggDataMap(this.subsTw, this.endTs);
+ aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType);
this.aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
index 00bc0c3e64..8414e6ff0e 100644
--- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
+++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
@@ -17,7 +17,6 @@
import {
Compiler,
Component,
- ComponentFactory,
Injectable,
Injector,
NgModule,
@@ -26,9 +25,9 @@ import {
Type,
ɵresetCompiledComponents
} from '@angular/core';
-import { from, Observable } from 'rxjs';
+import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common';
-import { catchError, map, mergeMap } from 'rxjs/operators';
+import { mergeMap } from 'rxjs/operators';
@NgModule()
export abstract class DynamicComponentModule implements OnDestroy {
@@ -79,33 +78,32 @@ export class DynamicComponentFactoryService {
declarations: [comp],
imports: moduleImports
})(class DynamicComponentInstanceModule extends DynamicComponentModule {});
- return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe(
- map((module) => {
- let moduleRef: NgModuleRef;
- try {
- moduleRef = module.create(this.injector);
- } catch (e) {
- this.compiler.clearCacheFor(module.moduleType);
- throw e;
- }
- this.dynamicComponentModulesMap.set(comp, {
- moduleRef,
- moduleType: module.moduleType
- });
- return {
- componentType: comp,
- componentModuleRef: moduleRef
- };
- }),
- catchError((error) => {
- if (compileAttempt === 1) {
- ɵresetCompiledComponents();
- return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
- } else {
- throw error;
- }
- })
- );
+ try {
+ const module = this.compiler.compileModuleSync(dynamicComponentInstanceModule);
+ let moduleRef: NgModuleRef;
+ try {
+ moduleRef = module.create(this.injector);
+ } catch (e) {
+ this.compiler.clearCacheFor(module.moduleType);
+ throw e;
+ }
+ this.dynamicComponentModulesMap.set(comp, {
+ moduleRef,
+ moduleType: module.moduleType
+ });
+ return of( {
+ componentType: comp,
+ componentModuleRef: moduleRef
+ });
+ } catch (error) {
+ if (compileAttempt === 1) {
+ ɵresetCompiledComponents();
+ return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
+ } else {
+ console.error(error);
+ throw error;
+ }
+ }
})
);
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
index 4dc332afa1..22920a609c 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
@@ -91,26 +91,16 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
index 6d4fe85fd5..fbc72d9498 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
@@ -40,9 +40,11 @@ import {
ObjectLwM2M,
OBSERVE,
PowerMode,
+ ObjectIDVer,
RESOURCES,
ServerSecurityConfig,
- TELEMETRY
+ TELEMETRY,
+ ObjectIDVerTranslationMap
} from './lwm2m-profile-config.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils';
@@ -80,6 +82,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
lwm2mDeviceProfileFormGroup: UntypedFormGroup;
configurationValue: Lwm2mProfileConfigModels;
+
+ objectIDVers = Object.values(ObjectIDVer) as ObjectIDVer[];
+ objectIDVerTranslationMap = ObjectIDVerTranslationMap;
+
sortFunction: (key: string, value: object) => object;
get required(): boolean {
@@ -116,7 +122,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
powerMode: [PowerMode.DRX, Validators.required],
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
- pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
+ pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required],
+ defaultObjectIDVer: [ObjectIDVer.V1_0, Validators.required]
})
});
@@ -274,7 +281,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE,
pagingTransmissionWindow:
this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW,
- psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER
+ psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER,
+ defaultObjectIDVer: this.configurationValue.clientLwM2mSettings.defaultObjectIDVer || ObjectIDVer.V1_0
}
},
{emitEvent: false});
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
index e5c292ef0a..8c1bb56a49 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
@@ -122,6 +122,18 @@ export const PowerModeTranslationMap = new Map(
]
);
+export enum ObjectIDVer {
+ V1_0 = '1.0',
+ V1_1 = '1.1'
+}
+
+export const ObjectIDVerTranslationMap = new Map(
+ [
+ [ObjectIDVer.V1_0, 'device-profile.lwm2m.default-object-id-ver.v1-0'],
+ [ObjectIDVer.V1_1, 'device-profile.lwm2m.default-object-id-ver.v1-1']
+ ]
+);
+
export interface ServerSecurityConfig {
host?: string;
port?: number;
@@ -163,7 +175,7 @@ export interface ClientLwM2mSettings {
edrxCycle?: number;
pagingTransmissionWindow?: number;
psmActivityTimer?: number;
- compositeOperationsSupport: boolean;
+ defaultObjectIDVer: ObjectIDVer;
}
export interface ObservableAttributes {
@@ -190,7 +202,7 @@ export function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSetting
fwUpdateStrategy: 1,
swUpdateStrategy: 1,
powerMode: PowerMode.DRX,
- compositeOperationsSupport: false
+ defaultObjectIDVer: ObjectIDVer.V1_0
};
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
index 428fb6fa1b..df63694df0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
@@ -15,6 +15,7 @@
///
import {
+ CellClickColumnInfo,
WidgetActionDescriptor,
WidgetActionSource,
widgetActionTypeTranslationMap
@@ -30,6 +31,7 @@ import { deepClone } from '@core/utils';
export interface WidgetActionCallbacks {
fetchDashboardStates: (query: string) => Array;
+ fetchCellClickColumns: () => Array;
}
export interface WidgetActionsData {
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
index f75b2e44e7..0330ad5621 100644
--- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
@@ -50,6 +50,27 @@
+