Browse Source

merged with edqs branch

pull/12701/head
dashevchenko 1 year ago
parent
commit
93e191856f
  1. 17
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md
  2. 108
      application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
  3. 2
      application/src/main/data/json/system/scada_symbols/control-panel-hp.svg
  4. 2
      application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
  5. 2
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  6. 108
      application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
  7. 2
      application/src/main/data/json/system/scada_symbols/leak-sensor.svg
  8. 2
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  9. 2
      application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg
  10. 2
      application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg
  11. 104
      application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
  12. 2
      application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg
  13. 2
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  14. 2
      application/src/main/data/json/system/scada_symbols/long-top-filter.svg
  15. 2
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  16. 2
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  17. 2
      application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg
  18. 2
      application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg
  19. 104
      application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
  20. 2
      application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg
  21. 2
      application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg
  22. 2
      application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg
  23. 2
      application/src/main/data/json/system/scada_symbols/short-top-filter.svg
  24. 2
      application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg
  25. 108
      application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
  26. 2
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  27. 108
      application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
  28. 1
      application/src/main/data/json/system/widget_bundles/buttons.json
  29. 1
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  30. 1
      application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json
  31. 2
      application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json
  32. 51
      application/src/main/data/json/system/widget_types/value_stepper.json
  33. 4
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  34. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  35. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  36. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  37. 18
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
  38. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  39. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  40. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java
  41. 4
      application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java
  42. 15
      application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java
  43. 10
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  44. 224
      application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java
  45. 1
      application/src/main/resources/logback.xml
  46. 37
      application/src/main/resources/thingsboard.yml
  47. 34
      application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java
  48. 3
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  49. 6
      application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java
  50. 10
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  51. 25
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  52. 40
      application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java
  53. 2
      application/src/test/resources/application-test.properties
  54. 2
      application/src/test/resources/logback-test.xml
  55. 9
      common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java
  56. 27
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java
  57. 16
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java
  58. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java
  59. 9
      common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java
  60. 5
      common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
  61. 3
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java
  62. 3
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java
  63. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java
  64. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java
  65. 154
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  66. 10
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java
  67. 10
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java
  68. 5
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java
  69. 3
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java
  70. 8
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java
  71. 153
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java
  72. 49
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java
  73. 7
      common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java
  74. 5
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java
  75. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java
  76. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java
  77. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java
  78. 19
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java
  79. 13
      common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java
  80. 71
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java
  81. 75
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java
  82. 96
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java
  83. 5
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java
  84. 49
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java
  85. 5
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java
  86. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  87. 1
      common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java
  88. 2
      common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java
  89. 6
      common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java
  90. 23
      common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java
  91. 4
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  92. 5
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
  93. 5
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
  94. 2
      common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java
  95. 10
      common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java
  96. 5
      dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java
  97. 4
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  98. 18
      dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java
  99. 65
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java
  100. 30
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java

17
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md

@ -43,19 +43,6 @@ volumes:
Execute the following command to start upgrade process:
```bash
docker compose -f docker-compose-upgrade.yml up
docker compose -f docker-compose-upgrade.yml up --abort-on-container-exit
{:copy-code}
```
Once upgrade process successfully completed, exit from the docker-compose shell by this combination:
```text
Ctrl + C
```
Execute the following command to stop TB Edge upgrade container:
```bash
docker compose -f docker-compose-upgrade.yml stop
{:copy-code}
```
```

108
application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

2
application/src/main/data/json/system/scada_symbols/control-panel-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Control panel",
"description": "Control panel",
"description": "Sends the command to the device or updates attribute/time series when the user pushes the button. Widget settings will enable you to configure behavior, how to fetch the initial state, and what to trigger when power on/off states.",
"searchTags": [
"control"
],

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

2
application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg

@ -776,7 +776,7 @@
}
]
}]]></tb:metadata>
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="784.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/>
<rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/>

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

2
application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Horizontal connector",
"description": "Horizontal connector",
"description": "Horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

108
application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 76 KiB

2
application/src/main/data/json/system/scada_symbols/leak-sensor.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" fill="none" version="1.1" viewBox="0 0 100 100"><tb:metadata xmlns=""><![CDATA[{
"title": "Leak sensor",
"description": "Leak sensor",
"description": "Leak sensor for real-time detection and warning of fluid leakage.",
"searchTags": [
"leak"
],

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

2
application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Left analog water level meter",
"description": "Left analog water level meter",
"description": "Left analog water level meter with real-time display of fluid levels.",
"searchTags": [
"water meter"
],

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

2
application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200">
<tb:metadata><![CDATA[{
"title": "Left drain pipe",
"description": "Left drain pipe",
"description": "Left drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

2
application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200">
<tb:metadata xmlns=""><![CDATA[{
"title": "Left elbow drain pipe",
"description": "Left elbow drain pipe",
"description": "Left elbow drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

104
application/src/main/data/json/system/scada_symbols/left-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,16 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width"
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"min": 0,
"step": 1
},
{
"id": "defaultBorderColor",
@ -510,17 +510,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "activeBorderColor",
@ -558,17 +548,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "warningBorderColor",
@ -606,17 +586,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "criticalBorderColor",
@ -654,17 +624,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "backgroundColor",
@ -702,49 +662,19 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#1EC1F480"
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#FFFFFF"
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600">
<tb:metadata xmlns=""><![CDATA[{
"title": "Long bottom filter",
"title": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"description": "Long bottom filter",
"searchTags": [
"filter"

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

2
application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg

@ -1,7 +1,7 @@
<svg width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata><![CDATA[{
"title": "HP Long horizontal connector",
"description": "Long horizontal connector",
"description": "Long horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 2,
"widgetSizeY": 1,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

2
application/src/main/data/json/system/scada_symbols/long-top-filter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600">
<tb:metadata xmlns=""><![CDATA[{
"title": "Long top filter",
"title": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"description": "Title\nLong top filter\n",
"searchTags": [
"filter"

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

2
application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Long vertical connector",
"description": "Long vertical connector",
"description": "Long vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 2,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

2
application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Right analog water level meter",
"description": "Right analog water level meter",
"description": "Right analog water level meter with real-time display of fluid levels.",
"searchTags": [
"water meter"
],

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

2
application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Right drain pipe",
"description": "Right drain pipe",
"description": "Right drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

2
application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200">
<tb:metadata xmlns=""><![CDATA[{
"title": "Right elbow drain pipe",
"description": "Right elbow drain pipe",
"description": "Right elbow drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

104
application/src/main/data/json/system/scada_symbols/right-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,16 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width"
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"min": 0,
"step": 1
},
{
"id": "defaultBorderColor",
@ -510,17 +510,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "activeBorderColor",
@ -558,17 +548,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "warningBorderColor",
@ -606,17 +586,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "criticalBorderColor",
@ -654,17 +624,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "backgroundColor",
@ -702,49 +662,19 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#1EC1F480"
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#FFFFFF"
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400">
<tb:metadata xmlns=""><![CDATA[{
"title": "Short bottom filter",
"description": "Short bottom filter",
"description": "Short bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"searchTags": [
"filter"
],

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

2
application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200">
<tb:metadata><![CDATA[{
"title": "Short left drain pipe",
"description": "Short left drain pipe",
"description": "Short left drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

2
application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200">
<tb:metadata><![CDATA[{
"title": "Short right drain pipe",
"description": "Short right drain pipe",
"description": "Short right drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

2
application/src/main/data/json/system/scada_symbols/short-top-filter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400">
<tb:metadata xmlns=""><![CDATA[{
"title": "Short top filter",
"title": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"description": "Short top filter",
"searchTags": [
"filter"

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

2
application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg

@ -704,7 +704,7 @@
}
]
}]]></tb:metadata>
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="786.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/>
<rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

108
application/src/main/data/json/system/scada_symbols/top-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Vertical connector",
"description": "Vertical connector",
"description": "Vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

108
application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 76 KiB

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

@ -12,6 +12,7 @@
"command_button",
"toggle_button",
"two_segment_button",
"value_stepper",
"power_button"
]
}

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

@ -12,6 +12,7 @@
"command_button",
"toggle_button",
"two_segment_button",
"value_stepper",
"power_button",
"slider",
"control_widgets.switch_control",

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

@ -29,6 +29,7 @@
"hp_top_tee_connector",
"hp_drawwork",
"hp_crane",
"hp_hook",
"hp_consumers",
"hp_house",
"hp_apartments",

2
application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json

File diff suppressed because one or more lines are too long

51
application/src/main/data/json/system/widget_types/value_stepper.json

@ -0,0 +1,51 @@
{
"fqn": "value_stepper",
"name": "Value stepper",
"deprecated": false,
"image": "tb-image;/api/images/system/value-stepper-widget.svg",
"description": "Allows users to click the buttons to send commands to devices or update attributes/time series data. Configurable settings let users define how to retrieve the initial state and specify actions for each button.",
"descriptor": {
"type": "rpc",
"sizeX": 3.5,
"sizeY": 2,
"resources": [],
"templateHtml": "<tb-value-stepper-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-value-stepper-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '230px',\n previewHeight: '110px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"dataKeySettingsForm": [],
"settingsDirective": "tb-value-stepper-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-value-stepper-basic-config",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"EXECUTE_RPC\",\"defaultValue\":0,\"executeRpc\":{\"method\":\"getState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return integer value */\\nreturn data;\"}},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"leftButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"rightButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"appearance\":{\"type\":\"simplified\",\"autoScale\":true,\"minValueRange\":-100,\"maxValueRange\":100,\"valueStep\":0.5,\"showValueBox\":true,\"valueUnits\":\"\",\"valueDecimals\":1,\"valueFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":16,\"sizeUnit\":\"px\",\"lineHeight\":\"24px\"},\"valueColor\":\"#000\",\"valueBoxBackground\":\"rgba(0, 0, 0, 0.04)\",\"showBorder\":true,\"borderWidth\":1,\"borderColor\":\"#305680\"},\"buttonAppearance\":{\"leftButton\":{\"showButton\":true,\"icon\":\"arrow_back_ios_new\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}},\"rightButton\":{\"showButton\":true,\"icon\":\"arrow_forward_ios\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}}},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\"},\"title\":\"Value stepper\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"configMode\":\"basic\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"datasources\":null,\"borderRadius\":null}"
},
"resources": [
{
"link": "/api/images/system/value-stepper-widget.svg",
"title": "\"Value stepper\" system widget image",
"type": "IMAGE",
"subType": "IMAGE",
"fileName": "value-stepper-widget.svg",
"publicResourceKey": "s0UKoqbiMCcKVn0pD55XZzPUR89XlXAO",
"mediaType": "image/svg+xml",
"data": "PHN2ZyB3aWR0aD0iMjE0IiBoZWlnaHQ9Ijc2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGZpbHRlcj0idXJsKCNhKSI+PHJlY3QgeD0iOC41IiB5PSI0LjUiIHdpZHRoPSIxOTciIGhlaWdodD0iNTkiIHJ4PSI0IiBmaWxsPSIjZmZmIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz48cmVjdCB4PSI5IiB5PSI1IiB3aWR0aD0iMTk2IiBoZWlnaHQ9IjU4IiByeD0iMy41IiBzdHJva2U9IiMzMDU2ODAiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxyZWN0IHg9IjIwLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0zOC41IDQwIDEuNDEtMS40MUwzNS4zMyAzNGw0LjU4LTQuNTlMMzguNSAyOGwtNiA2IDYgNloiIGZpbGw9IiNmZmYiLz48cmVjdCB4PSI2NSIgeT0iMTguNSIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjMxIiByeD0iMy41IiBmaWxsPSIjMzA1NjgwIiBmaWxsLW9wYWNpdHk9Ii4wNCIvPjxyZWN0IHg9IjY1IiB5PSIxOC41IiB3aWR0aD0iODQiIGhlaWdodD0iMzEiIHJ4PSIzLjUiIHN0cm9rZT0iIzMwNTY4MCIvPjxwYXRoIGQ9Ik04OC41OSAzNy41VjM5aC03LjYzdi0xLjI5bDMuNy00LjA0Yy40MS0uNDYuNzMtLjg1Ljk3LTEuMTkuMjMtLjMzLjQtLjYzLjQ5LS45YTIuMyAyLjMgMCAwIDAtLjA2LTEuNzNjLS4xMy0uMjctLjMyLS41LS41OC0uNjVhMS43IDEuNyAwIDAgMC0uOTMtLjI0Yy0uNDIgMC0uNzcuMS0xLjA2LjI3LS4yOC4xOS0uNS40NC0uNjUuNzZhMi42IDIuNiAwIDAgMC0uMjIgMS4xaC0xLjg4YzAtLjY3LjE1LTEuMjcuNDYtMS44Mi4zLS41NS43My0uOTkgMS4zLTEuM2E0LjEgNC4xIDAgMCAxIDIuMDgtLjVjLjc2IDAgMS40LjEzIDEuOTQuMzguNTMuMjYuOTMuNjIgMS4yIDEuMDlhMy4zNiAzLjM2IDAgMCAxIC4yNSAyLjcyIDUgNSAwIDAgMS0uNDkgMS4wNCA5IDkgMCAwIDEtLjc0IDEuMDRjLS4yOC4zNS0uNi43LS45NCAxLjA1bC0yLjQ2IDIuNzFoNS4yNVptOS4yNy05Ljg4djEuMDRMOTMuMyAzOWgtMS45OGw0LjU0LTkuODhoLTUuOXYtMS41aDcuODlabTEuOTggMTAuNDRjMC0uMjkuMS0uNTMuMy0uNzMuMi0uMi40Ni0uMy44LS4zcy42LjEuOC4zYy4yLjIuMy40NC4zLjczYTEgMSAwIDAgMS0uMy43NGMtLjIuMi0uNDYuMy0uOC4zcy0uNi0uMS0uOC0uM2ExIDEgMCAwIDEtLjMtLjc0Wm02LjQ5LTQuMzUtMS41LS4zNy42MS01LjcyaDYuMTR2MS42SDEwN2wtLjMyIDIuNzlhMy42NyAzLjY3IDAgMCAxIDEuODEtLjQ2Yy41NCAwIDEuMDIuMDkgMS40NC4yNi40Mi4xNy43OS40MyAxLjA4Ljc2LjMuMzMuNTMuNzMuNjggMS4yYTUgNSAwIDAgMSAwIDMuMDcgMy4xNyAzLjE3IDAgMCAxLTEuODUgMi4wM2MtLjQ2LjE5LTEuMDEuMjktMS42NS4yOWE0LjYgNC42IDAgMCAxLTEuMzYtLjIgMy43MyAzLjczIDAgMCAxLTEuMTctLjYyIDMuMTQgMy4xNCAwIDAgMS0xLjE5LTIuNDJoMS44NWMuMDUuMzcuMTUuNjkuMy45NS4xNi4yNS4zOC40NS42NC41OGEyIDIgMCAwIDAgLjkyLjJjLjMyIDAgLjYtLjA1LjgzLS4xNi4yMy0uMTEuNDItLjI3LjU3LS40OC4xNS0uMjIuMjctLjQ3LjM0LS43NWEzLjYyIDMuNjIgMCAwIDAtLjAyLTEuODcgMS45OCAxLjk4IDAgMCAwLS4zOC0uNzJjLS4xNy0uMi0uMzgtLjM2LS42My0uNDdhMi4xMyAyLjEzIDAgMCAwLS44OC0uMTdjLS40NSAwLS44LjA3LTEuMDQuMi0uMjMuMTMtLjQ1LjI5LS42NS40OFptMTEuNzUtNC4xNmMwLS4zOC4xLS43Mi4yOC0xLjA0LjE5LS4zMi40NC0uNTcuNzUtLjc2YTEuOTYgMS45NiAwIDAgMSAyLjA1IDBjLjMxLjE5LjU2LjQ0Ljc0Ljc2LjE5LjMyLjI4LjY2LjI4IDEuMDRzLS4xLjczLS4yOCAxLjA1Yy0uMTguMzEtLjQzLjU2LS43NC43NGEyLjA0IDIuMDQgMCAwIDEtMi44LS43NCAyLjAzIDIuMDMgMCAwIDEtLjI4LTEuMDVabTEuMDUgMGExIDEgMCAwIDAgMSAxIC45Ny45NyAwIDAgMCAuOTgtMSAxIDEgMCAwIDAtLjI3LS43Mi45My45MyAwIDAgMC0uNy0uM2MtLjI4IDAtLjUxLjEtLjcxLjMtLjIuMi0uMy40My0uMy43MlptMTIuMTkgNS43NWgxLjk1YTQuNSA0LjUgMCAwIDEtLjYyIDEuOTkgMy43MiAzLjcyIDAgMCAxLTEuNSAxLjM3IDUgNSAwIDAgMS0yLjMzLjUgNC4xNSA0LjE1IDAgMCAxLTMuMzQtMS40NWMtLjQtLjQ4LS43MS0xLjA0LS45Mi0xLjctLjIxLS42Ni0uMzItMS40LS4zMi0yLjIydi0uOTVjMC0uODEuMS0xLjU1LjMyLTIuMjIuMjItLjY2LjUzLTEuMjIuOTQtMS42OS40LS40Ny45LS44NCAxLjQ2LTEuMDlhNC43OCA0Ljc4IDAgMCAxIDEuOTMtLjM3Yy45IDAgMS42Ny4xNyAyLjMuNS42Mi4zMyAxLjEuOCAxLjQ1IDEuMzguMzUuNTkuNTYgMS4yNi42NCAyLjAyaC0xLjk1Yy0uMDUtLjQ4LS4xNy0uOS0uMzUtMS4yNWExLjc3IDEuNzcgMCAwIDAtLjc2LS44IDIuNzMgMi43MyAwIDAgMC0xLjMzLS4yOGMtLjQ1IDAtLjg0LjA4LTEuMTcuMjVhMi4yIDIuMiAwIDAgMC0uODQuNzNjLS4yMi4zMy0uMzkuNzItLjUgMS4yLS4xMS40Ny0uMTcgMS0uMTcgMS42di45N2MwIC41Ny4wNSAxLjEuMTUgMS41Ni4xLjQ3LjI2Ljg2LjQ3IDEuMi4yMS4zMy40OC41OS44MS43Ny4zMy4xOC43Mi4yNyAxLjE4LjI3LjU2IDAgMS0uMDggMS4zNS0uMjYuMzUtLjE4LjYxLS40NC44LS43OC4xNy0uMzQuMy0uNzYuMzUtMS4yNVoiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjg3Ii8+PHJlY3QgeD0iMTYxLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0xNzUuNSAyOC0xLjQxIDEuNDEgNC41OCA0LjU5LTQuNTggNC41OUwxNzUuNSA0MGw2LTYtNi02WiIgZmlsbD0iI2ZmZiIvPjwvZz48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIuNSIgeT0iLjUiIHdpZHRoPSIyMTMiIGhlaWdodD0iNzUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz48ZmVPZmZzZXQgZHk9IjQiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+PGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA0IDAiLz48ZmVCbGVuZCBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiIHJlc3VsdD0ic2hhcGUiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+",
"public": true
}
],
"scada": false,
"tags": [
"command",
"downlink",
"device configuration",
"device control",
"invocation",
"remote method",
"remote function",
"interface",
"subroutine call",
"inter-process communication",
"server request",
"update attribute",
"set attribute",
"add time-series"
]
}

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

@ -126,8 +126,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
final TenantId tenantId;
final DeviceId deviceId;
final LinkedHashMapRemoveEldest<UUID, SessionInfoMetaData> sessions;
private final Map<UUID, SessionInfo> attributeSubscriptions;
private final Map<UUID, SessionInfo> rpcSubscriptions;
final Map<UUID, SessionInfo> attributeSubscriptions;
final Map<UUID, SessionInfo> rpcSubscriptions;
private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
private final boolean rpcSequential;
private final RpcSubmitStrategy rpcSubmitStrategy;

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

@ -404,6 +404,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void scheduleEdgeEventsCheck(EdgeGrpcSession session) {
EdgeId edgeId = session.getEdge().getId();
TenantId tenantId = session.getEdge().getTenantId();
cancelScheduleEdgeEventsCheck(edgeId);
if (sessions.containsKey(edgeId)) {
ScheduledFuture<?> edgeEventCheckTask = edgeEventProcessingExecutorService.schedule(() -> {
try {

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

@ -448,7 +448,11 @@ public abstract class EdgeGrpcSession implements Closeable {
private void scheduleDownlinkMsgsPackSend(int attempt) {
Runnable sendDownlinkMsgsTask = () -> {
try {
if (isConnected() && !sessionState.getPendingMsgsMap().values().isEmpty()) {
if (!isConnected()) {
stopCurrentSendDownlinkMsgsTask(true);
return;
}
if (!sessionState.getPendingMsgsMap().values().isEmpty()) {
List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values());
if (attempt > 1) {
String error = "Failed to deliver the batch";
@ -529,6 +533,11 @@ public abstract class EdgeGrpcSession implements Closeable {
log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg);
} else {
log.error("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
DownlinkMsg downlinkMsg = sessionState.getPendingMsgsMap().get(msg.getDownlinkMsgId());
// if NOT timeseries or attributes failures - ack failed downlink
if (downlinkMsg.getEntityDataCount() == 0) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
}
}
if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId);

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

@ -87,6 +87,7 @@ public class EdgeSyncCursor {
fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService()));
fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService()));
fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService()));
fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
}

18
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java

@ -54,23 +54,25 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
log.trace("[{}] Finding general edge events [{}], seqIdStart = {}, pageLink = {}",
tenantId, edge.getId(), seqIdStart, pageLink);
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, null, (TimePageLink) pageLink);
if (edgeEvents.getData().isEmpty()) {
if (!edgeEvents.getData().isEmpty()) {
return edgeEvents;
}
if (seqIdStart > this.maxReadRecordsCount) {
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount), (TimePageLink) pageLink);
if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
this.seqIdNewCycleStarted = true;
this.seqIdStart = 0L;
} else {
edgeEvents = new PageData<>();
log.warn("[{}] unexpected edge notification message received. " +
"no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId());
return edgeEvents;
}
}
return edgeEvents;
log.info("[{}] Unexpected edge notification message received. " +
"No new events found, and the seqId column of the edge_event table has not started a new cycle [{}].", tenantId, edge.getId());
return new PageData<>();
} catch (Exception e) {
log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId());
log.error("[{}] Failed to find edge events [{}]", tenantId, edge.getId(), e);
return new PageData<>();
}
return new PageData<>();
}
}

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

@ -120,7 +120,7 @@ public abstract class BaseEdgeProcessor {
private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) {
return switch (action) {
case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT,
UPDATED_COMMENT -> true;
UPDATED_COMMENT, DELETED -> true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET,

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -41,7 +41,10 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
if (dashboard == null) {
throw new RuntimeException("[{" + tenantId + "}] dashboardUpdateMsg {" + dashboardUpdateMsg + "} cannot be converted to dashboard");
}
Set<ShortCustomerInfo> newAssignedCustomers = new HashSet<>(dashboard.getAssignedCustomers());
Set<ShortCustomerInfo> newAssignedCustomers = new HashSet<>();
if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) {
newAssignedCustomers.addAll(dashboard.getAssignedCustomers());
}
Dashboard dashboardById = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId);
if (dashboardById == null) {
created = true;

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java

@ -28,17 +28,22 @@ import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg;
public interface EdgeRequestsService {
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg);
ListenableFuture<Void> processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg);
ListenableFuture<Void> processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge, WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg);
}

4
application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java

@ -68,6 +68,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.HashPartitionService;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.edqs.EdqsQueue;
import org.thingsboard.server.queue.environment.DistributedLock;
import org.thingsboard.server.queue.environment.DistributedLockService;
@ -90,6 +91,7 @@ public class DefaultEdqsService implements EdqsService {
private final DistributedLockService distributedLockService;
private final AttributesService attributesService;
private final EdqsPartitionService edqsPartitionService;
private final TopicService topicService;
@Autowired @Lazy
private TbClusterService clusterService;
@Autowired @Lazy
@ -109,6 +111,7 @@ public class DefaultEdqsService implements EdqsService {
eventsProducer = EdqsProducer.builder()
.queue(EdqsQueue.EVENTS)
.partitionService(edqsPartitionService)
.topicService(topicService)
.producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS))
.build();
if (apiEnabled) {
@ -125,7 +128,6 @@ public class DefaultEdqsService implements EdqsService {
executor.submit(() -> {
try {
EdqsSyncState syncState = getSyncState();
// FIXME: Slavik smart events check
if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) {
if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) {
processSystemRequest(ToCoreEdqsRequest.builder()

15
application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.service.edqs;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.queue.edqs.EdqsQueue;
@ -26,22 +24,19 @@ import org.thingsboard.server.queue.kafka.TbKafkaSettings;
import java.util.Collections;
@Service
@RequiredArgsConstructor
@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'")
public class KafkaEdqsSyncService extends EdqsSyncService {
private final TbKafkaSettings kafkaSettings;
private TbKafkaAdmin kafkaAdmin;
private final boolean syncNeeded;
@PostConstruct
private void init() {
kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap());
public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings) {
TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap());
this.syncNeeded = kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic());
}
@Override
public boolean isSyncNeeded() {
return kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic());
return syncNeeded;
}
}

10
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java

@ -33,14 +33,14 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueEvent;
import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager;
import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask;
import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.DeleteQueueTask;
import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask;
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.queue.common.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;
@ -78,13 +78,13 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
}
public void delete(boolean drainQueue) {
addTask(TbQueueConsumerManagerTask.delete(drainQueue));
addTask(new DeleteQueueTask(drainQueue));
}
@Override
protected void processTask(TbQueueConsumerManagerTask task) {
if (task.getEvent() == QueueEvent.DELETE) {
doDelete(task.isDrainQueue());
if (task instanceof DeleteQueueTask deleteQueueTask) {
doDelete(deleteQueueTask.drainQueue());
}
}

224
application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java

@ -1,224 +0,0 @@
/**
* 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.sync.tenant;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.edqs.AttributeKv;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.edqs.LatestTsKv;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.dao.TenantEntityDao;
import org.thingsboard.server.dao.attributes.AttributesDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.entity.EntityDaoRegistry;
import org.thingsboard.server.dao.event.EventDao;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV;
import static org.thingsboard.server.common.data.ObjectType.AUDIT_LOG;
import static org.thingsboard.server.common.data.ObjectType.EVENT;
import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV;
import static org.thingsboard.server.common.data.ObjectType.RELATION;
import static org.thingsboard.server.common.data.ObjectType.TENANT;
@Service
@RequiredArgsConstructor
@Slf4j
public class TenantExportService {
private final EntityDaoRegistry entityDaoRegistry;
private final TenantDao tenantDao;
private final EventDao eventDao;
private final AuditLogDao auditLogDao;
private final AttributesDao attributesDao;
private final RelationDao relationDao;
private final TimeseriesLatestDao timeseriesLatestDao;
private final SqlPartitioningRepository partitioningRepository;
private Map<ObjectType, BiConsumer<TenantId, BiConsumer<ObjectType, Object>>> customExporters;
private Map<ObjectType, Exporter> relatedEntitiesExporters;
private static final Set<ObjectType> RELATED = EnumSet.of(EVENT, RELATION, ATTRIBUTE_KV, LATEST_TS_KV);
@PostConstruct
private void init() {
relatedEntitiesExporters = Map.of(
RELATION, this::exportRelations,
EVENT, this::exportEvents, // todo: query by tenant
ATTRIBUTE_KV, this::exportAttributes,
LATEST_TS_KV, this::exportLatestTelemetry
);
customExporters = Map.of(
AUDIT_LOG, this::exportAuditLogs
);
}
public void exportTenant(TenantId tenantId, ExportConfig config, BiConsumer<ObjectType, Object> processor) {
log.info("[{}] Exporting tenant", tenantId);
Tenant tenant = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId());
if (tenant == null) {
throw new IllegalArgumentException("Tenant with id " + tenantId + " not found");
}
Set<ObjectType> objectTypes = config.getIncludedObjectTypes();
if (objectTypes.contains(TENANT)) {
exportEntity(tenantId, TENANT, tenant, config, processor);
}
for (ObjectType type : objectTypes) {
if (RELATED.contains(type) || type == TENANT) {
continue;
}
log.debug("[{}] Exporting {} entities", tenantId, type);
if (!customExporters.containsKey(type)) {
TenantEntityDao<?> dao = entityDaoRegistry.getTenantEntityDao(type);
var entities = new PageDataIterable<>(pageLink -> dao.findAllByTenantId(tenantId, pageLink), 100);
for (Object entity : entities) {
exportEntity(tenantId, type, entity, config, processor);
}
} else {
customExporters.get(type).accept(tenantId, processor);
}
}
}
private void exportEntity(TenantId tenantId, ObjectType type, Object entity, ExportConfig config, BiConsumer<ObjectType, Object> processor) {
processor.accept(type, entity);
if (entity instanceof HasId<?> hasId && hasId.getId() instanceof EntityId entityId) {
relatedEntitiesExporters.forEach((relatedEntityType, exporter) -> {
if (config.getIncludedObjectTypes().contains(relatedEntityType)) {
exporter.export(tenantId, entityId, processor);
}
});
}
}
private Map<Long, Long> getPartitions(String table) {
List<Long> partitionsStartTime = partitioningRepository.fetchPartitions(table).stream().sorted().toList();
if (partitionsStartTime.isEmpty()) {
return Collections.emptyMap();
}
Map<Long, Long> partitions = new HashMap<>();
for (int i = 0; i < partitionsStartTime.size(); i++) {
Long startTime = partitionsStartTime.get(i);
Long endTime;
if (partitionsStartTime.size() - 1 == i) {
endTime = System.currentTimeMillis();
} else {
endTime = partitionsStartTime.get(i + 1) - 1;
}
partitions.put(startTime, endTime);
}
return partitions;
}
private void exportAuditLogs(TenantId tenantId, BiConsumer<ObjectType, Object> processor) {
Map<Long, Long> partitions = getPartitions(ModelConstants.AUDIT_LOG_TABLE_NAME);
partitions.forEach((startTime, endTime) -> {
PageDataIterable<AuditLog> auditLogs = new PageDataIterable<>(pageLink -> {
return auditLogDao.findAuditLogsByTenantId(tenantId.getId(), null, new TimePageLink(pageLink, startTime, endTime));
}, 512);
for (AuditLog auditLog : auditLogs) {
processor.accept(AUDIT_LOG, auditLog);
}
});
}
private void exportAttributes(TenantId tenantId, EntityId entityId, BiConsumer<ObjectType, Object> processor) {
for (AttributeScope attributeScope : AttributeScope.values()) {
List<AttributeKvEntry> attributes = attributesDao.findAll(tenantId, entityId, attributeScope);
for (AttributeKvEntry entry : attributes) {
AttributeKv attributeKv = new AttributeKv(entityId, attributeScope, entry, entry.getVersion());
processor.accept(ATTRIBUTE_KV, attributeKv);
}
}
}
private void exportRelations(TenantId tenantId, EntityId entityId, BiConsumer<ObjectType, Object> processor) {
List<EntityRelation> relations = relationDao.findAllByFrom(tenantId, entityId);
for (EntityRelation relation : relations) {
processor.accept(RELATION, relation);
}
}
@SneakyThrows
private void exportLatestTelemetry(TenantId tenantId, EntityId entityId, BiConsumer<ObjectType, Object> processor) {
List<TsKvEntry> latestTelemetry = timeseriesLatestDao.findAllLatest(tenantId, entityId).get(30, TimeUnit.SECONDS);
for (TsKvEntry tsKvEntry : latestTelemetry) {
LatestTsKv latestTsKv = new LatestTsKv(entityId, tsKvEntry, tsKvEntry.getVersion());
processor.accept(LATEST_TS_KV, latestTsKv);
}
}
private void exportEvents(TenantId tenantId, EntityId entityId, BiConsumer<ObjectType, Object> processor) {
for (EventType eventType : EventType.values()) {
Map<Long, Long> partitions = getPartitions(eventType.getTable());
partitions.forEach((startTime, endTime) -> {
PageDataIterable<? extends Event> events = new PageDataIterable<>(pageLink -> {
return eventDao.findEvents(tenantId.getId(), entityId.getId(), eventType, new TimePageLink(pageLink, startTime, endTime));
}, 512);
for (Event event : events) {
processor.accept(EVENT, event);
}
});
}
}
private interface Exporter {
void export(TenantId tenantId, EntityId entityId, BiConsumer<ObjectType, Object> processor);
}
@Data
public static class ExportConfig {
private Set<ObjectType> includedObjectTypes;
}
}

1
application/src/main/resources/logback.xml

@ -28,7 +28,6 @@
<logger name="org.thingsboard.server" level="INFO"/>
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/>
<logger name="org.apache.kafka.clients" level="WARN"/>
<logger name="org.thingsboard.server.actors" level="WARN"/>
<!-- To enable the logging of scanned rule engine components-->
<!-- <logger name="org.thingsboard.server.service.component.AnnotationComponentDiscoveryService" level="DEBUG" />-->
<!-- To enable the debug logging of rule node upgrade -->

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

@ -212,9 +212,9 @@ ui:
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records
ts:
type: "${DATABASE_TS_TYPE:sql}" # cassandra or sql. timescale option is deprecated and will no longer be supported in ThingsBoard 4.0
type: "${DATABASE_TS_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale)
ts_latest:
type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra or sql. timescale option is deprecated and will no longer be supported in ThingsBoard 4.0
type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale)
# Cassandra driver configuration parameters
cassandra:
@ -1696,25 +1696,38 @@ queue:
print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}"
edqs:
sync:
enabled: "${TB_EDQS_SYNC_ENABLED:true}" # Enable/disable EDQS synchronization with postgres db FIXME: disable by default before release
entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # batch size of entities being synced with EDQS
ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" # batch size of timeseries data being synced with EDQS
api_enabled: "${TB_EDQS_API_ENABLED:true}" # FIXME: disable by default before release
mode: "${TB_EDQS_MODE:local}" # local or remote
# Enable/disable EDQS synchronization
enabled: "${TB_EDQS_SYNC_ENABLED:false}"
# Batch size of entities being synced with EDQS
entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}"
# Batch size of timeseries data being synced with EDQS
ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}"
# Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation)
api_enabled: "${TB_EDQS_API_ENABLED:false}"
# Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices)
mode: "${TB_EDQS_MODE:local}"
local:
rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:/tmp/edqs-backup}"
# Path to RocksDB for EDQS backup when running in local mode
rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:${user.home}/.rocksdb/edqs}"
# Number of partitions for EDQS topics
partitions: "${TB_EDQS_PARTITIONS:12}"
partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data
# EDQS partitioning strategy: tenant (partition is resolved by tenant id) or none (no specific strategy, resolving by message key)
partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}"
# EDQS requests topic
requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}"
# EDQS responses topic
responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}"
# Poll interval for EDQS topics
poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}"
# Maximum amount of pending requests to EDQS
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}"
# Maximum timeout for requests to EDQS
max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}"
stats:
# Enable/disable statistics for EDQS service
# Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}"
# Statistics printing interval for EDQS
print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}"
print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}"
vc:
# Default topic name

34
application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java

@ -17,22 +17,27 @@ package org.thingsboard.server.actors.device;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.thingsboard.common.util.LinkedHashMapRemoveEldest;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.service.transport.TbCoreToTransportService;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
public class DeviceActorMessageProcessorTest {
public static final long MAX_CONCURRENT_SESSIONS_PER_DEVICE = 10L;
public static final int MAX_CONCURRENT_SESSIONS_PER_DEVICE = 10;
ActorSystemContext systemContext;
DeviceService deviceService;
TenantId tenantId = TenantId.SYS_TENANT_ID;
@ -44,15 +49,38 @@ public class DeviceActorMessageProcessorTest {
public void setUp() {
systemContext = mock(ActorSystemContext.class);
deviceService = mock(DeviceService.class);
willReturn(MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice();
willReturn((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice();
willReturn(deviceService).given(systemContext).getDeviceService();
processor = new DeviceActorMessageProcessor(systemContext, tenantId, deviceId);
willReturn(mock(TbCoreToTransportService.class)).given(systemContext).getTbCoreToTransportService();
}
@Test
public void givenSystemContext_whenNewInstance_thenVerifySessionMapMaxSize() {
assertThat(processor.sessions, instanceOf(LinkedHashMapRemoveEldest.class));
assertThat(processor.sessions.getMaxEntries(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.sessions.getMaxEntries(), is((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.sessions.getRemovalConsumer(), notNullValue());
}
@Test
public void givenFullSessionMap_whenSessionOverflow_thenShouldDeleteAttributeAndRPCSubscriptions() {
//givenFullSessionMap
for (int i = 0; i < MAX_CONCURRENT_SESSIONS_PER_DEVICE; i++) {
UUID sessionID = UUID.randomUUID();
processor.sessions.put(sessionID, Mockito.mock(SessionInfoMetaData.class, RETURNS_DEEP_STUBS));
processor.attributeSubscriptions.put(sessionID, Mockito.mock(SessionInfo.class));
processor.rpcSubscriptions.put(sessionID, Mockito.mock(SessionInfo.class));
}
assertThat(processor.sessions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.attributeSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.rpcSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
//add one more
processor.sessions.put(UUID.randomUUID(), Mockito.mock(SessionInfoMetaData.class));
assertThat(processor.sessions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.attributeSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE-1));
assertThat(processor.rpcSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE-1));
}
}

3
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -897,7 +897,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);
edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class);
edgeImitator.expectMessageAmount(26);
edgeImitator.expectMessageAmount(27);
edgeImitator.connect();
waitForMessages(edgeImitator);
@ -1003,6 +1003,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "jwt"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));

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

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edqs.state.EdqsStateService;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import java.util.concurrent.TimeUnit;
@ -44,12 +45,15 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest {
@Autowired
private EdqsService edqsService;
@Autowired(required = false)
private EdqsStateService edqsStateService;
@MockBean // so that we don't do backup for tests
private EdqsRocksDb edqsRocksDb;
@Before
public void before() {
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled());
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled() && edqsStateService.isReady());
}
@Override

10
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -144,7 +144,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);
edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class);
edgeImitator.expectMessageAmount(24);
edgeImitator.expectMessageAmount(26);
edgeImitator.connect();
requestEdgeRuleChainMetadata();
@ -265,10 +265,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
// 4 messages
// - 1 from default profile fetcher
// - 2 from device profile fetcher (default and thermostat)
// - 4 from device profile fetcher (2 * (default and thermostat) before and after ota packages fetcher
// - 1 from device fetcher
validateMsgsCnt(DeviceProfileUpdateMsg.class, 4);
validateDeviceProfiles(4);
validateMsgsCnt(DeviceProfileUpdateMsg.class, 6);
validateDeviceProfiles(6);
// 3 messages
// - 1 from default profile fetcher
@ -656,7 +656,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
protected RuleChainId createEdgeRuleChainAndAssignToEdge(String ruleChainName) throws Exception {
edgeImitator.expectMessageAmount(1);
edgeImitator.expectMessageAmount(2);
RuleChain ruleChain = new RuleChain();
ruleChain.setName(ruleChainName);
ruleChain.setType(RuleChainType.EDGE);

25
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -48,12 +48,14 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
private static final int MOBILE_ORDER = 5;
private static final String IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+";
private static final String DASHBOARD_TITLE = "Edge Test Dashboard";
@Test
public void testDashboards() throws Exception {
// create dashboard and assign to edge
edgeImitator.expectMessageAmount(2);
Dashboard dashboard = new Dashboard();
dashboard.setTitle("Edge Test Dashboard");
dashboard.setTitle(DASHBOARD_TITLE);
dashboard.setMobileHide(true);
dashboard.setImage(IMAGE);
dashboard.setMobileOrder(MOBILE_ORDER);
@ -200,12 +202,27 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertNotNull(foundDashboard);
Assert.assertEquals("Edge Test Dashboard", foundDashboard.getName());
Assert.assertEquals(DASHBOARD_TITLE, foundDashboard.getName());
PageData<DashboardInfo> pageData = doGetTypedWithPageLink("/api/customer/" + savedCustomer.getId().toString() + "/dashboards?",
new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals("Edge Test Dashboard", pageData.getData().get(0).getTitle());
Assert.assertEquals(DASHBOARD_TITLE, pageData.getData().get(0).getTitle());
dashboard.setTitle(DASHBOARD_TITLE + " Updated");
dashboard.setAssignedCustomers(null);
dashboardUpdateMsgBuilder.setEntity(JacksonUtil.toString(dashboard));
dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
uplinkMsgBuilder = UplinkMsg.newBuilder();
uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build());
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName());
}
@Test
@ -256,7 +273,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard dashboard = new Dashboard();
dashboard.setId(new DashboardId(UUID.randomUUID()));
dashboard.setTenantId(tenantId);
dashboard.setTitle("Edge Test Dashboard");
dashboard.setTitle(DASHBOARD_TITLE);
dashboard.setAssignedCustomers(Sets.newHashSet(new ShortCustomerInfo(savedCustomer.getId(), savedCustomer.getTitle(), savedCustomer.isPublic())));
return dashboard;
}

40
application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java

@ -126,6 +126,45 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
unAssignFromEdgeAndDeleteDashboard(thermostatsDashboardId);
}
@Test
public void testDeleteDeviceProfilesWhenEdgeIsOffline() throws Exception {
RuleChainId thermostatsRuleChainId = createEdgeRuleChainAndAssignToEdge("Thermostats Rule Chain");
// create device profile
DeviceProfile deviceProfile = this.createDeviceProfile("ONE_MORE_DEVICE_PROFILE", null);
deviceProfile.setDefaultEdgeRuleChainId(thermostatsRuleChainId);
extendDeviceProfileData(deviceProfile);
edgeImitator.expectMessageAmount(1);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg);
DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage;
DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true);
Assert.assertNotNull(deviceProfileMsg);
Assert.assertEquals(deviceProfile, deviceProfileMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
// delete profile when edge is offline
edgeImitator.disconnect();
doDelete("/api/deviceProfile/" + deviceProfile.getUuidId())
.andExpect(status().isOk());
edgeImitator.connect();
// 27 sync message
// + 1 delete message
edgeImitator.expectMessageAmount(28);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg);
deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
unAssignFromEdgeAndDeleteRuleChain(thermostatsRuleChainId);
}
@Test
public void testDeviceProfiles_snmp() throws Exception {
DeviceProfile deviceProfile = createDeviceProfileAndDoBasicAssert("SNMP", createSnmpDeviceProfileTransportConfiguration());
@ -449,4 +488,5 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
deviceProfile.setProfileData(createProfileData());
return deviceProfile;
}
}

2
application/src/test/resources/application-test.properties

@ -53,7 +53,7 @@ sql.ttl.audit_logs.ttl=2592000
sql.edge_events.partition_size=168
sql.ttl.edge_events.edge_event_ttl=2592000
server.log_controller_error_stack_trace=true
server.log_controller_error_stack_trace=false
transport.gateway.dashboard.sync.enabled=false

2
application/src/test/resources/logback-test.xml

@ -9,7 +9,7 @@
<!-- <logger name="org.thingsboard.server.service.subscription" level="TRACE"/>-->
<logger name="org.thingsboard.server.controller.TbTestWebSocketClient" level="INFO"/>
<logger name="org.thingsboard.server" level="INFO"/>
<logger name="org.thingsboard.server" level="WARN"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.springframework.boot.test" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/>

9
common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java

@ -23,7 +23,6 @@ public enum ObjectType {
TENANT,
TENANT_PROFILE,
CUSTOMER,
ADMIN_SETTINGS,
QUEUE,
RPC,
RULE_CHAIN,
@ -32,8 +31,6 @@ public enum ObjectType {
EVENT,
RULE_NODE,
USER,
USER_CREDENTIALS,
USER_AUTH_SETTINGS,
EDGE,
WIDGETS_BUNDLE,
WIDGET_TYPE,
@ -54,7 +51,6 @@ public enum ObjectType {
NOTIFICATION_TEMPLATE,
NOTIFICATION_RULE,
ALARM_COMMENT,
ALARM_TYPE,
API_USAGE_STATE,
QUEUE_STATS,
@ -70,7 +66,10 @@ public enum ObjectType {
public static final Set<ObjectType> edqsTypes = EnumSet.copyOf(edqsTenantTypes);
public static final Set<ObjectType> edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD,
API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV);
public static final Set<ObjectType> unversionedTypes = EnumSet.of(QUEUE_STATS);
public static final Set<ObjectType> unversionedTypes = EnumSet.of(
QUEUE_STATS, // created once, never updated
TENANT_PROFILE // only for total count calculation
);
static {
edqsTypes.addAll(List.of(RELATION, ATTRIBUTE_KV, LATEST_TS_KV));

27
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java

@ -1,27 +0,0 @@
/**
* 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.alarm;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class AlarmType {
private TenantId tenantId;
private String type;
}

16
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java

@ -15,27 +15,15 @@
*/
package org.thingsboard.server.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
import java.util.UUID;
@Data
@NoArgsConstructor
@SuperBuilder
public class GenericFields extends AbstractEntityFields {
private String additionalInfo;
public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo) {
super(id, createdTime, tenantId, name, version);
this.additionalInfo = getText(additionalInfo);
}
public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version) {
super(id, createdTime, tenantId, name, version);
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java

@ -56,10 +56,6 @@ public class TenantFields extends AbstractEntityFields {
this.region = region;
}
public TenantFields(UUID id, Long version) {
super(id, 0L, null, version);
}
@Override
public UUID getTenantId() {
return getId();

9
common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java

@ -23,7 +23,6 @@ import java.util.NoSuchElementException;
public abstract class BasePageDataIterable<T> implements Iterable<T>, Iterator<T> {
private final int fetchSize;
private SortOrder sortOrder;
private List<T> currentItems;
private int currentIdx;
@ -36,12 +35,6 @@ public abstract class BasePageDataIterable<T> implements Iterable<T>, Iterator<T
this.fetchSize = fetchSize;
}
public BasePageDataIterable(int fetchSize, SortOrder sortOrder) {
super();
this.fetchSize = fetchSize;
this.sortOrder = sortOrder;
}
@Override
public Iterator<T> iterator() {
return this;
@ -50,7 +43,7 @@ public abstract class BasePageDataIterable<T> implements Iterable<T>, Iterator<T
@Override
public boolean hasNext() {
if (!initialized) {
fetch(new PageLink(fetchSize, 0, null, sortOrder));
fetch(new PageLink(fetchSize));
initialized = true;
}
if (currentIdx == currentItems.size()) {

5
common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java

@ -24,11 +24,6 @@ public class PageDataIterable<T> extends BasePageDataIterable<T> {
this.function = function;
}
public PageDataIterable(FetchFunction<T> function, int fetchSize, SortOrder sortOrder) {
super(fetchSize, sortOrder);
this.function = function;
}
@Override
PageData<T> fetchPageData(PageLink link) {
return function.fetch(link);

3
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java

@ -39,7 +39,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonSubTypes.Type(value = AssetSearchQueryFilter.class, name = "assetSearchQuery"),
@JsonSubTypes.Type(value = DeviceSearchQueryFilter.class, name = "deviceSearchQuery"),
@JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery"),
@JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery")})
@JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery")
})
public interface EntityFilter {
@JsonIgnore

3
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java

@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data.dp;
import lombok.Getter;
import lombok.SneakyThrows;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.util.TbBytePool;
import org.xerial.snappy.Snappy;
public class CompressedStringDataPoint extends AbstractDataPoint {
@ -29,7 +30,7 @@ public class CompressedStringDataPoint extends AbstractDataPoint {
@SneakyThrows
public CompressedStringDataPoint(long ts, byte[] compressedValue) {
super(ts);
this.compressedValue = compressedValue;
this.compressedValue = TbBytePool.intern(compressedValue);
}
@Override

2
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java

@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp;
import lombok.Getter;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.repo.TbStringPool;
import org.thingsboard.server.edqs.util.TbStringPool;
public class JsonDataPoint extends AbstractDataPoint {

2
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java

@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp;
import lombok.Getter;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.repo.TbStringPool;
import org.thingsboard.server.edqs.util.TbStringPool;
public class StringDataPoint extends AbstractDataPoint {

154
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -24,12 +24,10 @@ import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ExceptionUtil;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -43,14 +41,13 @@ import org.thingsboard.server.common.data.edqs.query.QueryResult;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.edqs.repo.EdqRepository;
import org.thingsboard.server.edqs.repo.EdqsRepository;
import org.thingsboard.server.edqs.state.EdqsPartitionService;
import org.thingsboard.server.edqs.state.EdqsStateService;
import org.thingsboard.server.edqs.util.EdqsConverter;
import org.thingsboard.server.edqs.state.EdqsPartitionService;
import org.thingsboard.server.edqs.util.VersionsStore;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg;
@ -59,7 +56,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.TbQueueHandler;
import org.thingsboard.server.queue.TbQueueResponseTemplate;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.edqs.EdqsComponent;
@ -69,6 +66,7 @@ import org.thingsboard.server.queue.edqs.EdqsQueue;
import org.thingsboard.server.queue.edqs.EdqsQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@ -78,6 +76,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic;
@EdqsComponent
@Service
@RequiredArgsConstructor
@ -86,25 +86,23 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
private final EdqsQueueFactory queueFactory;
private final EdqsConverter converter;
private final EdqRepository repository;
private final EdqsRepository repository;
private final EdqsConfig config;
private final EdqsPartitionService partitionService;
private final ConfigurableApplicationContext applicationContext;
@Autowired @Lazy
private EdqsStateService stateService;
private final EdqsStateService stateService;
private MainQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>, QueueConfig> eventsConsumer;
private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer;
private TbQueueResponseTemplate<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>> responseTemplate;
private ExecutorService consumersExecutor;
private ExecutorService mgmtExecutor;
private ExecutorService taskExecutor;
private ScheduledExecutorService scheduler;
private ListeningExecutorService requestExecutor;
private ExecutorService repartitionExecutor;
private final VersionsStore versionsStore = new VersionsStore();
private final AtomicInteger counter = new AtomicInteger(); // FIXME: TMP
private final AtomicInteger counter = new AtomicInteger();
@Getter
private Consumer<Throwable> errorHandler;
@ -112,10 +110,9 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
@PostConstruct
private void init() {
consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer"));
mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-mgmt");
taskExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-task-executor");
scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler");
requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests"));
repartitionExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-repartition"));
errorHandler = error -> {
if (error instanceof OutOfMemoryError) {
log.error("OOM detected, shutting down");
@ -125,14 +122,17 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
}
};
eventsConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>, QueueConfig>builder()
eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>create()
.queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic()))
.config(QueueConfig.of(true, config.getPollInterval()))
.topic(EdqsQueue.EVENTS.getTopic())
.pollInterval(config.getPollInterval())
.msgPackProcessor((msgs, consumer, config) -> {
for (TbProtoQueueMsg<ToEdqsMsg> queueMsg : msgs) {
if (consumer.isStopped()) {
return;
}
try {
ToEdqsMsg msg = queueMsg.getValue();
log.trace("Processing message: {}", msg);
process(msg, EdqsQueue.EVENTS);
} catch (Exception t) {
log.error("Failed to process message: {}", queueMsg, t);
@ -142,10 +142,12 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
})
.consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS))
.consumerExecutor(consumersExecutor)
.taskExecutor(mgmtExecutor)
.taskExecutor(taskExecutor)
.scheduler(scheduler)
.uncaughtErrorHandler(errorHandler)
.build();
stateService.init(eventConsumer);
responseTemplate = queueFactory.createEdqsResponseTemplate();
}
@ -159,64 +161,50 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
if (event.getServiceType() != ServiceType.EDQS) {
return;
}
repartitionExecutor.submit(() -> { // todo: maybe cancel the task if new event comes
try {
Set<TopicPartitionInfo> newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS));
Set<TopicPartitionInfo> partitions = newPartitions.stream()
.map(tpi -> tpi.withUseInternalPartition(true))
.collect(Collectors.toSet());
try {
Set<TopicPartitionInfo> newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS));
Set<TopicPartitionInfo> partitions = newPartitions.stream()
.map(tpi -> tpi.withUseInternalPartition(true))
.collect(Collectors.toSet());
try {
stateService.restore(withTopic(partitions, EdqsQueue.STATE.getTopic())); // blocks until restored
} catch (Exception e) {
log.error("Failed to process restore for partitions {}", partitions, e);
}
eventsConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic()));
responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic()));
stateService.process(withTopic(partitions, EdqsQueue.STATE.getTopic()));
// eventsConsumer's partitions are updated by stateService
responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); // FIXME: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template
Set<TopicPartitionInfo> oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS));
if (CollectionsUtil.isNotEmpty(oldPartitions)) {
Set<Integer> removedPartitions = Sets.difference(oldPartitions, newPartitions).stream()
.map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet());
if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) {
log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions);
}
repository.clearIf(tenantId -> {
Integer partition = partitionService.resolvePartition(tenantId);
return partition != null && removedPartitions.contains(partition);
});
Set<TopicPartitionInfo> oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS));
if (CollectionsUtil.isNotEmpty(oldPartitions)) {
Set<Integer> removedPartitions = Sets.difference(oldPartitions, newPartitions).stream()
.map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet());
if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) {
log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions);
}
} catch (Throwable t) {
log.error("Failed to handle partition change event {}", event, t);
repository.clearIf(tenantId -> {
Integer partition = partitionService.resolvePartition(tenantId);
return partition != null && removedPartitions.contains(partition);
});
}
});
} catch (Throwable t) {
log.error("Failed to handle partition change event {}", event, t);
}
}
@Override
public ListenableFuture<TbProtoQueueMsg<FromEdqsMsg>> handle(TbProtoQueueMsg<ToEdqsMsg> queueMsg) {
ToEdqsMsg toEdqsMsg = queueMsg.getValue();
return requestExecutor.submit(() -> {
EdqsResponse response = new EdqsResponse();
EdqsRequest request;
TenantId tenantId;
CustomerId customerId;
try {
EdqsRequest request = JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class);
TenantId tenantId = getTenantId(toEdqsMsg);
CustomerId customerId = getCustomerId(toEdqsMsg);
log.info("[{}] Handling request: {}", tenantId, request);
if (request.getEntityDataQuery() != null) {
PageData<QueryResult> result = repository.findEntityDataByQuery(tenantId, customerId,
request.getEntityDataQuery(), false);
response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData));
} else if (request.getEntityCountQuery() != null) {
long result = repository.countEntitiesByQuery(tenantId, customerId, request.getEntityCountQuery(), tenantId.isSysTenantId());
response.setEntityCountQueryResult(result);
}
log.info("Answering with response: {}", response);
} catch (Throwable e) {
response.setError(ExceptionUtils.getStackTrace(e)); // TODO: return only the message
log.info("Answering with error", e);
request = Objects.requireNonNull(JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class));
tenantId = getTenantId(toEdqsMsg);
customerId = getCustomerId(toEdqsMsg);
} catch (Exception e) {
log.error("Failed to parse request msg: {}", toEdqsMsg, e);
throw e;
}
EdqsResponse response = processRequest(tenantId, customerId, request);
return new TbProtoQueueMsg<>(queueMsg.getKey(), FromEdqsMsg.newBuilder()
.setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder()
.setValue(JacksonUtil.toString(response))
@ -225,7 +213,27 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
});
}
private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) {
EdqsResponse response = new EdqsResponse();
try {
if (request.getEntityDataQuery() != null) {
PageData<QueryResult> result = repository.findEntityDataByQuery(tenantId, customerId,
request.getEntityDataQuery(), false);
response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData));
} else if (request.getEntityCountQuery() != null) {
long result = repository.countEntitiesByQuery(tenantId, customerId, request.getEntityCountQuery(), tenantId.isSysTenantId());
response.setEntityCountQueryResult(result);
}
log.trace("[{}] Request: {}, response: {}", tenantId, request, response);
} catch (Throwable e) {
log.error("[{}] Failed to process request: {}", tenantId, request, e);
response.setError(ExceptionUtil.getMessage(e));
}
return response;
}
public void process(ToEdqsMsg edqsMsg, EdqsQueue queue) {
log.trace("Processing message: {}", edqsMsg);
if (edqsMsg.hasEventMsg()) {
EdqsEventMsg eventMsg = edqsMsg.getEventMsg();
TenantId tenantId = getTenantId(edqsMsg);
@ -274,23 +282,17 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
}
}
private Set<TopicPartitionInfo> withTopic(Set<TopicPartitionInfo> partitions, String topic) {
return partitions.stream()
.map(tpi -> tpi.withTopic(topic))
.collect(Collectors.toSet());
}
@PreDestroy
public void destroy() throws InterruptedException {
eventsConsumer.stop();
eventsConsumer.awaitStop();
eventConsumer.stop();
eventConsumer.awaitStop();
responseTemplate.stop();
stateService.stop();
consumersExecutor.shutdownNow();
mgmtExecutor.shutdownNow();
taskExecutor.shutdownNow();
scheduler.shutdownNow();
requestExecutor.shutdownNow();
repartitionExecutor.shutdownNow();
}
}

10
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java

@ -27,6 +27,7 @@ import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.edqs.EdqsQueue;
import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate;
@ -35,26 +36,27 @@ public class EdqsProducer {
private final EdqsQueue queue;
private final EdqsPartitionService partitionService;
private final TopicService topicService;
private final TbQueueProducer<TbProtoQueueMsg<ToEdqsMsg>> producer;
@Builder
public EdqsProducer(EdqsQueue queue,
EdqsPartitionService partitionService,
TopicService topicService,
TbQueueProducer<TbProtoQueueMsg<ToEdqsMsg>> producer) {
this.queue = queue;
this.partitionService = partitionService;
this.topicService = topicService;
this.producer = producer;
}
// TODO: queue prefix!
public void send(TenantId tenantId, ObjectType type, String key, ToEdqsMsg msg) {
String topic = queue.getTopic();
String topic = topicService.buildTopicName(queue.getTopic());
TbQueueCallback callback = new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); // fixme log levels
log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg);
}
@Override

10
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java → common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java

@ -39,7 +39,7 @@ import java.util.function.Predicate;
@AllArgsConstructor
@Service
@Slf4j
public class InMemoryEdqRepository implements EdqRepository {
public class DefaultEdqsRepository implements EdqsRepository {
private final static ConcurrentMap<TenantId, TenantRepo> repos = new ConcurrentHashMap<>();
private final Optional<EdqsStatsService> statsService;
@ -51,7 +51,7 @@ public class InMemoryEdqRepository implements EdqRepository {
@Override
public void processEvent(EdqsEvent event) {
if (event.getEventType() == EdqsEventType.DELETED && event.getObjectType() == ObjectType.TENANT) {
log.info("Deleting tenant repo: {}", event);
log.info("Tenant {} deleted", event.getTenantId());
repos.remove(event.getTenantId());
} else {
get(event.getTenantId()).processEvent(event);
@ -63,17 +63,17 @@ public class InMemoryEdqRepository implements EdqRepository {
long startNs = System.nanoTime();
long result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck);
double timingMs = (double) (System.nanoTime() - startNs) / 1000_000;
log.info("countEntitiesByQuery: {} ms", timingMs);
log.info("countEntitiesByQuery done in {} ms", timingMs);
return result;
}
@Override
public PageData<QueryResult> findEntityDataByQuery(TenantId tenantId, CustomerId customerId,
EntityDataQuery query, boolean ignorePermissionCheck) {
EntityDataQuery query, boolean ignorePermissionCheck) {
long startNs = System.nanoTime();
var result = get(tenantId).findEntityDataByQuery(customerId, query, ignorePermissionCheck);
double timingMs = (double) (System.nanoTime() - startNs) / 1000_000;
log.info("findEntityDataByQuery: {} ms", timingMs);
log.info("findEntityDataByQuery done in {} ms", timingMs);
return result;
}

5
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java → common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java

@ -25,13 +25,10 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
import java.util.function.Predicate;
public interface EdqRepository {
public interface EdqsRepository {
void processEvent(EdqsEvent event);
@Deprecated
default void addOrUpdate(TenantId tenantId, Object object) {}
long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck);
PageData<QueryResult> findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, boolean ignorePermissionCheck);

3
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java

@ -57,6 +57,7 @@ import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor;
import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory;
import org.thingsboard.server.edqs.stats.EdqsStatsService;
import org.thingsboard.server.edqs.util.RepositoryUtils;
import org.thingsboard.server.edqs.util.TbStringPool;
import java.util.ArrayList;
import java.util.Collections;
@ -104,7 +105,7 @@ public class TenantRepo {
public void processEvent(EdqsEvent event) {
EdqsObject edqsObject = event.getObject();
log.debug("[{}] Processing event: {}", tenantId, event);
log.trace("[{}] Processing event: {}", tenantId, event);
if (event.getEventType() == EdqsEventType.UPDATED) {
addOrUpdate(edqsObject);
} else if (event.getEventType() == EdqsEventType.DELETED) {

8
common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java

@ -20,15 +20,21 @@ import org.thingsboard.server.common.data.edqs.EdqsEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
import java.util.Set;
public interface EdqsStateService {
void restore(Set<TopicPartitionInfo> partitions);
void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer);
void process(Set<TopicPartitionInfo> partitions);
void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg);
boolean isReady();
void stop();
}

153
common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java

@ -15,17 +15,14 @@
*/
package org.thingsboard.server.edqs.state;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.edqs.EdqsEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.edqs.processor.EdqsProcessor;
@ -34,9 +31,11 @@ import org.thingsboard.server.edqs.util.VersionsStore;
import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.common.consumer.QueueStateService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.edqs.EdqsConfig;
import org.thingsboard.server.queue.edqs.EdqsQueue;
import org.thingsboard.server.queue.edqs.EdqsQueueFactory;
@ -44,10 +43,6 @@ import org.thingsboard.server.queue.edqs.KafkaEdqsComponent;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
@Service
@ -59,121 +54,106 @@ public class KafkaEdqsStateService implements EdqsStateService {
private final EdqsConfig config;
private final EdqsPartitionService partitionService;
private final EdqsQueueFactory queueFactory;
private final EdqsProcessor edqsProcessor;
private final TopicService topicService;
@Autowired @Lazy
private EdqsProcessor edqsProcessor;
private MainQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>, QueueConfig> stateConsumer;
private QueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventsConsumer;
private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> stateConsumer;
private QueueStateService<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<ToEdqsMsg>> queueStateService;
private QueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventsToBackupConsumer;
private EdqsProducer stateProducer;
private boolean initialRestoreDone;
private ExecutorService consumersExecutor;
private ExecutorService mgmtExecutor;
private ScheduledExecutorService scheduler;
private final VersionsStore versionsStore = new VersionsStore();
private final AtomicInteger stateReadCount = new AtomicInteger();
private final AtomicInteger eventsReadCount = new AtomicInteger();
private Boolean ready;
@PostConstruct
private void init() {
consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer"));
mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt");
scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler");
stateConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>, QueueConfig>builder() // FIXME Slavik: if topic is empty
@Override
public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer) {
stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>create()
.queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic()))
.config(QueueConfig.of(true, config.getPollInterval()))
.topic(EdqsQueue.STATE.getTopic())
.pollInterval(config.getPollInterval())
.msgPackProcessor((msgs, consumer, config) -> {
for (TbProtoQueueMsg<ToEdqsMsg> queueMsg : msgs) {
try {
ToEdqsMsg msg = queueMsg.getValue();
log.trace("Processing message: {}", msg);
edqsProcessor.process(msg, EdqsQueue.STATE);
if (stateReadCount.incrementAndGet() % 100000 == 0) {
log.info("[state] Processed {} msgs", stateReadCount.get());
}
} catch (Exception e) {
log.error("Failed to process message: {}", queueMsg, e); // TODO: do something about the error - e.g. reprocess
log.error("Failed to process message: {}", queueMsg, e);
}
}
consumer.commit();
})
.consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE))
.consumerExecutor(consumersExecutor)
.taskExecutor(mgmtExecutor)
.scheduler(scheduler)
.consumerExecutor(eventConsumer.getConsumerExecutor())
.taskExecutor(eventConsumer.getTaskExecutor())
.scheduler(eventConsumer.getScheduler())
.uncaughtErrorHandler(edqsProcessor.getErrorHandler())
.build();
queueStateService = new QueueStateService<>();
queueStateService.init(stateConsumer, eventConsumer);
ExecutorService backupExecutor = ThingsBoardExecutors.newLimitedTasksExecutor(12, 1000, "events-to-backup-executor");
eventsConsumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>builder() // FIXME Slavik writes to the state while we read it, slows down the start. maybe start backup consumer after restore is finished
eventsToBackupConsumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>builder()
.name("edqs-events-to-backup-consumer")
.pollInterval(config.getPollInterval())
.msgPackProcessor((msgs, consumer) -> {
CountDownLatch resultLatch = new CountDownLatch(msgs.size());
for (TbProtoQueueMsg<ToEdqsMsg> queueMsg : msgs) {
backupExecutor.submit(() -> {
try {
ToEdqsMsg msg = queueMsg.getValue();
log.trace("Processing message: {}", msg);
if (msg.hasEventMsg()) {
EdqsEventMsg eventMsg = msg.getEventMsg();
String key = eventMsg.getKey();
int count = eventsReadCount.incrementAndGet();
if (count % 100000 == 0) {
log.info("[events-to-backup] Processed {} msgs", count);
}
if (eventMsg.hasVersion()) {
if (!versionsStore.isNew(key, eventMsg.getVersion())) {
return;
}
}
if (consumer.isStopped()) {
return;
}
try {
ToEdqsMsg msg = queueMsg.getValue();
log.trace("Processing message: {}", msg);
TenantId tenantId = getTenantId(msg);
ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType());
EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType());
log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key);
stateProducer.send(tenantId, objectType, key, msg);
if (msg.hasEventMsg()) {
EdqsEventMsg eventMsg = msg.getEventMsg();
String key = eventMsg.getKey();
int count = eventsReadCount.incrementAndGet();
if (count % 100000 == 0) {
log.info("[events-to-backup] Processed {} msgs", count);
}
} catch (Throwable t) {
log.error("Failed to process message: {}", queueMsg, t);
} finally {
resultLatch.countDown();
if (eventMsg.hasVersion()) {
if (!versionsStore.isNew(key, eventMsg.getVersion())) {
continue;
}
}
TenantId tenantId = getTenantId(msg);
ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType());
EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType());
log.trace("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key);
stateProducer.send(tenantId, objectType, key, msg);
}
});
} catch (Throwable t) {
log.error("Failed to process message: {}", queueMsg, t);
}
}
resultLatch.await();
consumer.commit();
})
.consumerCreator(() -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group
.consumerExecutor(consumersExecutor)
.consumerExecutor(eventConsumer.getConsumerExecutor())
.threadPrefix("edqs-events-to-backup")
.build();
stateProducer = EdqsProducer.builder()
.queue(EdqsQueue.STATE)
.partitionService(partitionService)
.topicService(topicService)
.producer(queueFactory.createEdqsMsgProducer(EdqsQueue.STATE))
.build();
}
@Override
public void restore(Set<TopicPartitionInfo> partitions) {
stateReadCount.set(0); //TODO Slavik: do not support remote mode in monolith setup
long startTs = System.currentTimeMillis();
log.info("Restore started for partitions {}", partitions.stream().map(tpi -> tpi.getPartition().orElse(-1)).sorted().toList());
stateConsumer.doUpdate(partitions); // calling blocking doUpdate instead of update
stateConsumer.awaitStop(0); // consumers should stop on their own because EdqsQueue.STATE.stopWhenRead is true, we just need to wait
log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), stateReadCount.get());
if (!initialRestoreDone) {
initialRestoreDone = true;
eventsConsumer.subscribe();
eventsConsumer.launch();
public void process(Set<TopicPartitionInfo> partitions) {
if (queueStateService.getPartitions() == null) {
eventsToBackupConsumer.subscribe();
eventsToBackupConsumer.launch();
}
queueStateService.update(partitions);
}
@Override
@ -183,23 +163,26 @@ public class KafkaEdqsStateService implements EdqsStateService {
@Override
public boolean isReady() {
return initialRestoreDone;
if (ready == null) {
Set<TopicPartitionInfo> partitionsInProgress = queueStateService.getPartitionsInProgress();
if (partitionsInProgress != null && partitionsInProgress.isEmpty()) {
ready = true; // once true - always true, not to change readiness status on each repartitioning
}
}
log.error("ready: {}", ready);
return ready != null && ready;
}
private TenantId getTenantId(ToEdqsMsg edqsMsg) {
return TenantId.fromUUID(new UUID(edqsMsg.getTenantIdMSB(), edqsMsg.getTenantIdLSB()));
}
@PreDestroy
private void preDestroy() {
@Override
public void stop() {
stateConsumer.stop();
stateConsumer.awaitStop();
eventsConsumer.stop();
eventsToBackupConsumer.stop();
stateProducer.stop();
consumersExecutor.shutdownNow();
mgmtExecutor.shutdownNow();
scheduler.shutdownNow();
}
}

49
common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java

@ -27,40 +27,49 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.edqs.processor.EdqsProcessor;
import org.thingsboard.server.edqs.util.EdqsRocksDb;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
import org.thingsboard.server.queue.edqs.EdqsQueue;
import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent;
import java.util.Set;
import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic;
@Service
@RequiredArgsConstructor
@InMemoryEdqsComponent
@Slf4j
public class LocalEdqsStateService implements EdqsStateService {
private final EdqsRocksDb db;
@Autowired @Lazy
private EdqsProcessor processor;
@Autowired
private EdqsRocksDb db;
private boolean restoreDone;
private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer;
private Set<TopicPartitionInfo> partitions;
@Override
public void restore(Set<TopicPartitionInfo> partitions) {
if (restoreDone) {
return;
}
public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer) {
this.eventConsumer = eventConsumer;
}
db.forEach((key, value) -> {
try {
ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value);
log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg);
processor.process(edqsMsg, EdqsQueue.STATE);
} catch (Exception e) {
log.error("[{}] Failed to restore value", key, e);
}
});
restoreDone = true;
@Override
public void process(Set<TopicPartitionInfo> partitions) {
if (this.partitions == null) {
db.forEach((key, value) -> {
try {
ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value);
log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg);
processor.process(edqsMsg, EdqsQueue.STATE);
} catch (Exception e) {
log.error("[{}] Failed to restore value", key, e);
}
});
log.info("Restore completed");
}
eventConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic()));
this.partitions = partitions;
}
@Override
@ -79,7 +88,11 @@ public class LocalEdqsStateService implements EdqsStateService {
@Override
public boolean isReady() {
return restoreDone;
return partitions != null;
}
@Override
public void stop() {
}
}

7
common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java

@ -43,9 +43,12 @@ public class EdqsStatsService {
private final ConcurrentHashMap<TenantId, EdqsStats> statsMap = new ConcurrentHashMap<>();
private final StatsFactory statsFactory;
@Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:60000}",
fixedDelayString = "${queue.edqs.stats.print-interval-ms:60000}")
@Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:300000}",
fixedDelayString = "${queue.edqs.stats.print-interval-ms:300000}")
private void reportStats() {
if (statsMap.isEmpty()) {
return;
}
String values = statsMap.entrySet().stream()
.map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]")
.collect(Collectors.joining(System.lineSeparator()));

5
common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java

@ -42,7 +42,6 @@ import org.thingsboard.server.edqs.data.dp.DoubleDataPoint;
import org.thingsboard.server.edqs.data.dp.JsonDataPoint;
import org.thingsboard.server.edqs.data.dp.LongDataPoint;
import org.thingsboard.server.edqs.data.dp.StringDataPoint;
import org.thingsboard.server.edqs.repo.TbBytePool;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto;
import org.xerial.snappy.Snappy;
@ -161,11 +160,11 @@ public class EdqsConverter {
} else if (proto.hasStringV()) {
return new StringDataPoint(ts, proto.getStringV());
} else if (proto.hasCompressedStringV()) {
return new CompressedStringDataPoint(ts, TbBytePool.intern(proto.getCompressedStringV().toByteArray()));
return new CompressedStringDataPoint(ts, proto.getCompressedStringV().toByteArray());
} else if (proto.hasJsonV()) {
return new JsonDataPoint(ts, proto.getJsonV());
} else if (proto.hasCompressedJsonV()) {
return new CompressedJsonDataPoint(ts, TbBytePool.intern(proto.getCompressedJsonV().toByteArray()));
return new CompressedJsonDataPoint(ts, proto.getCompressedJsonV().toByteArray());
} else {
throw new IllegalArgumentException("Unsupported data point proto: " + proto);
}

2
common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java

@ -33,7 +33,7 @@ public class EdqsRocksDb extends TbRocksDb {
@Getter
private boolean isNew;
public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${java.io.tmpdir}/edqs-backup}") String path) {
public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${user.home}/.rocksdb/edqs}") String path) {
super(path, new Options().setCreateIfMissing(true));
}

2
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java → common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edqs.repo;
package org.thingsboard.server.edqs.util;
import com.google.common.hash.Hashing;
import org.springframework.util.ConcurrentReferenceHashMap;

2
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java → common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edqs.repo;
package org.thingsboard.server.edqs.util;
import org.springframework.util.ConcurrentReferenceHashMap;

19
common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java

@ -17,13 +17,13 @@ package org.thingsboard.server.common.msg.queue;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ToString
public class TopicPartitionInfo {
private final String topic;
@ -58,7 +58,7 @@ public class TopicPartitionInfo {
}
public TopicPartitionInfo newByTopic(String topic) {
return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.myPartition);
return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition);
}
public String getTopic() {
@ -77,6 +77,10 @@ public class TopicPartitionInfo {
return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition);
}
public static Set<TopicPartitionInfo> withTopic(Set<TopicPartitionInfo> partitions, String topic) {
return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet());
}
public TopicPartitionInfo withUseInternalPartition(boolean useInternalPartition) {
return new TopicPartitionInfo(this.topic, this.tenantId, this.partition, useInternalPartition, this.myPartition);
}
@ -97,4 +101,13 @@ public class TopicPartitionInfo {
return Objects.hash(fullTopicName, partition);
}
@Override
public String toString() {
String str = fullTopicName;
if (useInternalPartition) {
str += "[" + partition + "]";
}
return str;
}
}

13
common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java

@ -31,7 +31,6 @@ import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
@ -95,7 +94,7 @@ public abstract class AbstractTbQueueConsumerTemplate<R, T extends TbQueueMsg> i
partitions = subscribeQueue.poll();
}
if (!subscribed) {
log.info("Subscribing to topics {}", getFullTopicNames());
log.info("Subscribing to {}", partitions);
doSubscribe(partitions);
subscribed = true;
}
@ -148,6 +147,9 @@ public abstract class AbstractTbQueueConsumerTemplate<R, T extends TbQueueMsg> i
@Override
public void commit() {
if (consumerLock.isLocked()) {
if (stopped) {
return;
}
log.error("commit. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock topic " + topic, new RuntimeException("stacktrace"));
}
consumerLock.lock();
@ -165,7 +167,7 @@ public abstract class AbstractTbQueueConsumerTemplate<R, T extends TbQueueMsg> i
@Override
public void unsubscribe() {
log.info("Unsubscribing and stopping consumer for topics {}", getFullTopicNames());
log.info("Unsubscribing and stopping consumer for {}", partitions);
stopped = true;
consumerLock.lock();
try {
@ -198,9 +200,8 @@ public abstract class AbstractTbQueueConsumerTemplate<R, T extends TbQueueMsg> i
return Collections.emptyList();
}
return partitions.stream()
.map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ?
"[" + tpi.getPartition().orElse(-1) + "]" : ""))
.collect(Collectors.toList());
.map(TopicPartitionInfo::getFullTopicName)
.toList();
}
protected boolean isLongPollingSupported() {

71
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java

@ -23,6 +23,8 @@ 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.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask;
import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask;
import org.thingsboard.server.queue.discovery.QueueKey;
import java.util.Collection;
@ -31,6 +33,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
@ -40,7 +43,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j
public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfig> {
@ -51,8 +53,11 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
protected C config;
protected final MsgPackProcessor<M, C> msgPackProcessor;
protected final BiFunction<C, Integer, TbQueueConsumer<M>> consumerCreator;
@Getter
protected final ExecutorService consumerExecutor;
@Getter
protected final ScheduledExecutorService scheduler;
@Getter
protected final ExecutorService taskExecutor;
protected final Consumer<Throwable> uncaughtErrorHandler;
@ -87,20 +92,24 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
public void init(C config) {
this.config = config;
this.consumerWrapper = createConsumerWrapper(config);
log.debug("[{}] Initialized consumer for queue: {}", queueKey, config);
}
protected ConsumerWrapper<M> createConsumerWrapper(C config) {
if (config.isConsumerPerPartition()) {
this.consumerWrapper = new ConsumerPerPartitionWrapper();
return new ConsumerPerPartitionWrapper();
} else {
this.consumerWrapper = new SingleConsumerWrapper();
return new SingleConsumerWrapper();
}
log.debug("[{}] Initialized consumer for queue: {}", queueKey, config);
}
public void update(C config) {
addTask(TbQueueConsumerManagerTask.configUpdate(config));
addTask(new UpdateConfigTask(config));
}
public void update(Set<TopicPartitionInfo> partitions) {
addTask(TbQueueConsumerManagerTask.partitionChange(partitions));
addTask(new UpdatePartitionsTask(partitions));
}
protected void addTask(TbQueueConsumerManagerTask todo) {
@ -125,10 +134,10 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
}
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();
if (task instanceof UpdatePartitionsTask updatePartitionsTask) {
newPartitions = updatePartitionsTask.partitions();
} else if (task instanceof UpdateConfigTask updateConfigTask) {
newConfig = (C) updateConfigTask.config();
} else {
processTask(task);
}
@ -184,7 +193,7 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
}
}
public void doUpdate(Set<TopicPartitionInfo> partitions) {
private void doUpdate(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
consumerWrapper.updatePartitions(partitions);
}
@ -195,6 +204,15 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString());
consumerLoop(consumerTask.getConsumer());
log.info("[{}] Consumer stopped", consumerTask.getKey());
try {
Runnable callback = consumerTask.getCallback();
if (callback != null) {
callback.run();
}
} catch (Throwable t) {
log.error("Failed to execute finish callback", t);
}
});
consumerTask.setTask(consumerLoop);
}
@ -245,18 +263,12 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
awaitStop(30);
}
public void awaitStop(int timeoutSec) {
private void awaitStop(int timeoutSec) {
log.debug("[{}] Waiting for consumers to stop", queueKey);
consumerWrapper.getConsumers().forEach(consumerTask -> consumerTask.awaitCompletion(timeoutSec));
log.debug("[{}] Unsubscribed and stopped consumers", queueKey);
}
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) {
return partitions.stream().map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ?
"[" + tpi.getPartition().orElse(-1) + "]" : ""))
.collect(Collectors.joining(", ", "[", "]"));
}
public interface MsgPackProcessor<M extends TbQueueMsg, C extends QueueConfig> {
void process(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception;
}
@ -279,15 +291,24 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
Set<TopicPartitionInfo> 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());
log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, addedPartitions, removedPartitions);
removePartitions(removedPartitions);
addPartitions(addedPartitions, null);
}
protected void removePartitions(Set<TopicPartitionInfo> removedPartitions) {
removedPartitions.forEach((tpi) -> Optional.ofNullable(consumers.get(tpi)).ifPresent(TbQueueConsumerTask::initiateStop));
removedPartitions.forEach((tpi) -> Optional.ofNullable(consumers.remove(tpi)).ifPresent(TbQueueConsumerTask::awaitCompletion));
}
addedPartitions.forEach((tpi) -> {
protected void addPartitions(Set<TopicPartitionInfo> partitions, Consumer<TopicPartitionInfo> onStop) {
partitions.forEach(tpi -> {
Integer partitionId = tpi.getPartition().orElse(-1);
String key = queueKey + "-" + partitionId;
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId));
Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null;
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId), callback);
consumers.put(tpi, consumer);
consumer.subscribe(Set.of(tpi));
launchConsumer(consumer);
@ -305,7 +326,7 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions));
log.info("[{}] New partitions: {}", queueKey, partitions);
if (partitions.isEmpty()) {
if (consumer != null && consumer.isRunning()) {
consumer.initiateStop();
@ -316,7 +337,7 @@ public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfi
}
if (consumer == null) {
consumer = new TbQueueConsumerTask<>(queueKey, () -> consumerCreator.apply(config, null)); // no partitionId passed
consumer = new TbQueueConsumerTask<>(queueKey, () -> consumerCreator.apply(config, null), null); // no partitionId passed
}
consumer.subscribe(partitions);
if (!consumer.isRunning()) {

75
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java

@ -0,0 +1,75 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.common.consumer;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
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.common.consumer.TbQueueConsumerManagerTask.AddPartitionsTask;
import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.RemovePartitionsTask;
import org.thingsboard.server.queue.discovery.QueueKey;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@Slf4j
public class PartitionedQueueConsumerManager<M extends TbQueueMsg> extends MainQueueConsumerManager<M, QueueConfig> {
private final ConsumerPerPartitionWrapper consumerWrapper;
@Getter
private final String topic;
@Builder(builderMethodName = "create") // not to conflict with super.builder()
public PartitionedQueueConsumerManager(QueueKey queueKey, String topic, long pollInterval, MsgPackProcessor<M, QueueConfig> msgPackProcessor,
BiFunction<QueueConfig, Integer, TbQueueConsumer<M>> consumerCreator,
ExecutorService consumerExecutor, ScheduledExecutorService scheduler,
ExecutorService taskExecutor, Consumer<Throwable> uncaughtErrorHandler) {
super(queueKey, QueueConfig.of(true, pollInterval), msgPackProcessor, consumerCreator, consumerExecutor, scheduler, taskExecutor, uncaughtErrorHandler);
this.topic = topic;
this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper;
}
@Override
protected void processTask(TbQueueConsumerManagerTask task) {
if (task instanceof AddPartitionsTask addPartitionsTask) {
log.info("[{}] Added partitions: {}", queueKey, addPartitionsTask.partitions());
consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop());
} else if (task instanceof RemovePartitionsTask removePartitionsTask) {
log.info("[{}] Removed partitions: {}", queueKey, removePartitionsTask.partitions());
consumerWrapper.removePartitions(removePartitionsTask.partitions());
}
}
public void addPartitions(Set<TopicPartitionInfo> partitions) {
addPartitions(partitions, null);
}
public void addPartitions(Set<TopicPartitionInfo> partitions, Consumer<TopicPartitionInfo> onStop) {
addTask(new AddPartitionsTask(partitions, onStop));
}
public void removePartitions(Set<TopicPartitionInfo> partitions) {
addTask(new RemovePartitionsTask(partitions));
}
}

96
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.common.consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueMsg;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic;
@Slf4j
public class QueueStateService<E extends TbQueueMsg, S extends TbQueueMsg> {
private PartitionedQueueConsumerManager<S> stateConsumer;
private PartitionedQueueConsumerManager<E> eventConsumer;
@Getter
private Set<TopicPartitionInfo> partitions;
private final Set<TopicPartitionInfo> partitionsInProgress = ConcurrentHashMap.newKeySet();
private boolean initialized;
private final Lock lock = new ReentrantLock();
public void init(PartitionedQueueConsumerManager<S> stateConsumer, PartitionedQueueConsumerManager<E> eventConsumer) {
this.stateConsumer = stateConsumer;
this.eventConsumer = eventConsumer;
}
public void update(Set<TopicPartitionInfo> newPartitions) {
newPartitions = withTopic(newPartitions, stateConsumer.getTopic());
lock.lock();
Set<TopicPartitionInfo> oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet();
Set<TopicPartitionInfo> addedPartitions;
Set<TopicPartitionInfo> removedPartitions;
try {
addedPartitions = new HashSet<>(newPartitions);
addedPartitions.removeAll(oldPartitions);
removedPartitions = new HashSet<>(oldPartitions);
removedPartitions.removeAll(newPartitions);
this.partitions = newPartitions;
} finally {
lock.unlock();
}
if (!removedPartitions.isEmpty()) {
stateConsumer.removePartitions(removedPartitions);
eventConsumer.removePartitions(withTopic(removedPartitions, eventConsumer.getTopic()));
}
if (!addedPartitions.isEmpty()) {
partitionsInProgress.addAll(addedPartitions);
stateConsumer.addPartitions(addedPartitions, partition -> {
lock.lock();
try {
partitionsInProgress.remove(partition);
log.info("Finished partition {} (still in progress: {})", partition, partitionsInProgress);
if (partitionsInProgress.isEmpty()) {
log.info("All partitions processed");
}
if (this.partitions.contains(partition)) {
eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic())));
}
} finally {
lock.unlock();
}
});
}
initialized = true;
}
public Set<TopicPartitionInfo> getPartitionsInProgress() {
return initialized ? partitionsInProgress : null;
}
}

5
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java → common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java

@ -17,8 +17,9 @@ package org.thingsboard.server.queue.common.consumer;
import java.io.Serializable;
public enum QueueEvent implements Serializable {
public enum QueueTaskType implements Serializable {
PARTITION_CHANGE, CONFIG_UPDATE, DELETE
UPDATE_PARTITIONS, UPDATE_CONFIG, DELETE,
ADD_PARTITIONS, REMOVE_PARTITIONS
}

49
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java

@ -15,34 +15,49 @@
*/
package org.thingsboard.server.queue.common.consumer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import java.util.Set;
import java.util.function.Consumer;
@Getter
@ToString
@AllArgsConstructor
public class TbQueueConsumerManagerTask {
public interface TbQueueConsumerManagerTask {
private final QueueEvent event;
private QueueConfig config;
private Set<TopicPartitionInfo> partitions;
private boolean drainQueue;
QueueTaskType getType();
public static TbQueueConsumerManagerTask delete(boolean drainQueue) {
return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue);
record DeleteQueueTask(boolean drainQueue) implements TbQueueConsumerManagerTask {
@Override
public QueueTaskType getType() {
return QueueTaskType.DELETE;
}
}
public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) {
return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false);
record UpdateConfigTask(QueueConfig config) implements TbQueueConsumerManagerTask {
@Override
public QueueTaskType getType() {
return QueueTaskType.UPDATE_CONFIG;
}
}
public static TbQueueConsumerManagerTask partitionChange(Set<TopicPartitionInfo> partitions) {
return new TbQueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, null, partitions, false);
record UpdatePartitionsTask(Set<TopicPartitionInfo> partitions) implements TbQueueConsumerManagerTask {
@Override
public QueueTaskType getType() {
return QueueTaskType.UPDATE_PARTITIONS;
}
}
record AddPartitionsTask(Set<TopicPartitionInfo> partitions, Consumer<TopicPartitionInfo> onStop) implements TbQueueConsumerManagerTask {
@Override
public QueueTaskType getType() {
return QueueTaskType.ADD_PARTITIONS;
}
}
record RemovePartitionsTask(Set<TopicPartitionInfo> partitions) implements TbQueueConsumerManagerTask {
@Override
public QueueTaskType getType() {
return QueueTaskType.REMOVE_PARTITIONS;
}
}
}

5
common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java

@ -35,14 +35,17 @@ public class TbQueueConsumerTask<M extends TbQueueMsg> {
private final Object key;
private volatile TbQueueConsumer<M> consumer;
private volatile Supplier<TbQueueConsumer<M>> consumerSupplier;
@Getter
private final Runnable callback;
@Setter
private Future<?> task;
public TbQueueConsumerTask(Object key, Supplier<TbQueueConsumer<M>> consumerSupplier) {
public TbQueueConsumerTask(Object key, Supplier<TbQueueConsumer<M>> consumerSupplier, Runnable callback) {
this.key = key;
this.consumer = null;
this.consumerSupplier = consumerSupplier;
this.callback = callback;
}
public TbQueueConsumer<M> getConsumer() {

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

@ -59,7 +59,7 @@ import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME;
@Slf4j
public class HashPartitionService implements PartitionService {
@Value("${queue.core.topic}")
@Value("${queue.core.topic:tb_core}")
private String coreTopic;
@Value("${queue.core.partitions:10}")
private Integer corePartitions;

1
common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java

@ -21,7 +21,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
// TODO: tb-core ?
@ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " +
"(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " +
"'${queue.edqs.mode:null}'=='local'))")

2
common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java

@ -38,7 +38,7 @@ public class EdqsConfig {
private long pollInterval;
@Value("${queue.edqs.max_pending_requests:10000}")
private int maxPendingRequests;
@Value("${queue.edqs.max_request_timeout:10000}")
@Value("${queue.edqs.max_request_timeout:20000}")
private int maxRequestTimeout;
public String getLabel() {

6
common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java

@ -18,7 +18,8 @@ package org.thingsboard.server.queue.edqs;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.stats.DummyMessagesStats;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
@ -37,6 +38,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory {
private final InMemoryStorage storage;
private final EdqsConfig edqsConfig;
private final StatsFactory statsFactory;
@Override
public TbQueueConsumer<TbProtoQueueMsg<ToEdqsMsg>> createEdqsMsgConsumer(EdqsQueue queue) {
@ -69,7 +71,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory {
.maxPendingRequests(edqsConfig.getMaxPendingRequests())
.requestTimeout(edqsConfig.getMaxRequestTimeout())
.pollInterval(edqsConfig.getPollInterval())
.stats(new DummyMessagesStats()) // FIXME
.stats(statsFactory.createMessagesStats(StatsType.EDQS.getName()))
.executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs"))
.build();
}

23
common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java

@ -17,7 +17,8 @@ package org.thingsboard.server.queue.edqs;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.stats.DummyMessagesStats;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg;
@ -27,6 +28,7 @@ import org.thingsboard.server.queue.TbQueueResponseTemplate;
import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.kafka.TbKafkaAdmin;
import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService;
import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate;
@ -47,12 +49,15 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory {
private final EdqsConfig edqsConfig;
private final TbServiceInfoProvider serviceInfoProvider;
private final TbKafkaConsumerStatsService consumerStatsService;
private final TopicService topicService;
private final StatsFactory statsFactory;
private final AtomicInteger consumerCounter = new AtomicInteger();
public KafkaEdqsQueueFactory(TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs topicConfigs,
EdqsConfig edqsConfig, TbServiceInfoProvider serviceInfoProvider,
TbKafkaConsumerStatsService consumerStatsService) {
TbKafkaConsumerStatsService consumerStatsService, TopicService topicService,
StatsFactory statsFactory) {
this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsEventsConfigs());
this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsRequestsConfigs());
this.edqsStateAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsStateConfigs());
@ -60,6 +65,8 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory {
this.edqsConfig = edqsConfig;
this.serviceInfoProvider = serviceInfoProvider;
this.consumerStatsService = consumerStatsService;
this.topicService = topicService;
this.statsFactory = statsFactory;
}
@Override
@ -72,11 +79,11 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory {
public TbQueueConsumer<TbProtoQueueMsg<ToEdqsMsg>> createEdqsMsgConsumer(EdqsQueue queue, String group) {
return TbKafkaConsumerTemplate.<TbProtoQueueMsg<ToEdqsMsg>>builder()
.settings(kafkaSettings)
.topic(queue.getTopic())
.topic(topicService.buildTopicName(queue.getTopic()))
.readFromBeginning(queue.isReadFromBeginning())
.stopWhenRead(queue.isStopWhenRead())
.clientId("edqs-" + queue.name().toLowerCase() + "-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId())
.groupId(group)
.groupId(topicService.buildTopicName(group))
.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders()))
.admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin)
.statsService(consumerStatsService)
@ -97,16 +104,16 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory {
String requestsConsumerGroup = "edqs-requests-consumer-group-" + edqsConfig.getLabel();
var requestConsumer = TbKafkaConsumerTemplate.<TbProtoQueueMsg<TransportProtos.ToEdqsMsg>>builder()
.settings(kafkaSettings)
.topic(edqsConfig.getRequestsTopic())
.topic(topicService.buildTopicName(edqsConfig.getRequestsTopic()))
.clientId("edqs-requests-consumer-" + serviceInfoProvider.getServiceId())
.groupId(requestsConsumerGroup)
.groupId(topicService.buildTopicName(requestsConsumerGroup))
.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders()))
.admin(edqsRequestsAdmin)
.statsService(consumerStatsService);
var responseProducer = TbKafkaProducerTemplate.<TbProtoQueueMsg<FromEdqsMsg>>builder()
.settings(kafkaSettings)
.clientId("edqs-response-producer-" + serviceInfoProvider.getServiceId())
.defaultTopic(edqsConfig.getResponsesTopic())
.defaultTopic(topicService.buildTopicName(edqsConfig.getResponsesTopic()))
.admin(edqsRequestsAdmin);
return DefaultTbQueueResponseTemplate.<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>>builder()
.requestTemplate(requestConsumer.build())
@ -114,7 +121,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory {
.maxPendingRequests(edqsConfig.getMaxPendingRequests())
.requestTimeout(edqsConfig.getMaxRequestTimeout())
.pollInterval(edqsConfig.getPollInterval())
.stats(new DummyMessagesStats()) // FIXME
.stats(statsFactory.createMessagesStats(StatsType.EDQS.getName()))
.executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs"))
.build();
}

4
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java

@ -54,6 +54,7 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
private final boolean readFromBeginning; // reset offset to beginning
private final boolean stopWhenRead; // stop consuming when reached an empty msg pack
private int readCount;
private Map<Integer, Long> endOffsets; // needed if stopWhenRead is true
private boolean partitionsAssigned = false;
@ -152,6 +153,7 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
records.forEach(record -> {
recordList.add(record);
if (stopWhenRead) {
readCount++;
int partition = record.partition();
Long endOffset = endOffsets.get(partition);
if (endOffset == null) {
@ -166,7 +168,7 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
});
}
if (stopWhenRead && endOffsets.isEmpty()) {
log.info("Reached end offset for {}, stopping consumer", consumer.assignment());
log.info("Finished reading {}, processed {} messages", partitions, readCount);
stop();
}
return recordList;

5
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java

@ -103,6 +103,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
private final TbKafkaAdmin edqsRequestsAdmin;
private final AtomicLong consumerCount = new AtomicLong();
private final AtomicLong edgeConsumerCount = new AtomicLong();
public KafkaMonolithQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings,
TbServiceInfoProvider serviceInfoProvider,
@ -483,7 +484,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId));
consumerBuilder.clientId("monolith-to-edge-event-consumer" + serviceInfoProvider.getServiceId());
consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(edgeEventAdmin);
@ -521,7 +522,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
var responseConsumer = TbKafkaConsumerTemplate.<TbProtoQueueMsg<FromEdqsMsg>>builder()
.settings(kafkaSettings)
.topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId()))
.clientId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId()))
.clientId("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())
.groupId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId()))
.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders()))
.admin(edqsRequestsAdmin)

5
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java

@ -102,6 +102,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
private final TbKafkaAdmin edqsRequestsAdmin;
private final AtomicLong consumerCount = new AtomicLong();
private final AtomicLong edgeConsumerCount = new AtomicLong();
public KafkaTbCoreQueueFactory(TopicService topicService,
TbKafkaSettings kafkaSettings,
@ -432,7 +433,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId));
consumerBuilder.clientId("tb-core-edge-event-consumer" + serviceInfoProvider.getServiceId());
consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(edgeEventAdmin);
@ -470,7 +471,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
var responseConsumer = TbKafkaConsumerTemplate.<TbProtoQueueMsg<FromEdqsMsg>>builder()
.settings(kafkaSettings)
.topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId()))
.clientId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId()))
.clientId("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())
.groupId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId()))
.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders()))
.admin(edqsRequestsAdmin)

2
common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java

@ -38,7 +38,7 @@ public class DefaultStatsFactory implements StatsFactory {
private static final Counter STUB_COUNTER = new StubCounter();
@Autowired(required = false) // FIXME Slavik !!!
@Autowired
private MeterRegistry meterRegistry;
@Value("${metrics.enabled:false}")

10
common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java

@ -64,4 +64,14 @@ public class ExceptionUtil {
}
}
}
public static String getMessage(Throwable t) {
String message = t.getMessage();
if (StringUtils.isNotEmpty(message)) {
return message;
} else {
return t.getClass().getSimpleName();
}
}
}

5
dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -30,8 +29,4 @@ public interface TenantEntityDao<T> {
throw new UnsupportedOperationException();
}
default ObjectType getType() {
throw new UnsupportedOperationException();
}
}

4
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -142,12 +142,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
private EdqsResponse processEdqsRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) {
EdqsResponse response;
try {
log.info("Sending request to EDQS: {}", request);
log.debug("[{}] Sending request to EDQS: {}", tenantId, request);
response = edqsService.processRequest(tenantId, customerId, request).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
log.info("Received response from EDQS: {}", response);
log.debug("[{}] Received response from EDQS: {}", tenantId, response);
if (response.getError() != null) {
throw new RuntimeException(response.getError());
}

18
dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java

@ -18,9 +18,7 @@ package org.thingsboard.server.dao.entity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.TenantEntityDao;
import java.util.EnumMap;
import java.util.List;
@ -31,20 +29,14 @@ import java.util.Map;
@SuppressWarnings({"unchecked"})
public class EntityDaoRegistry {
private final Map<ObjectType, TenantEntityDao<?>> tenantEntityDaos = new EnumMap<>(ObjectType.class);
private final Map<EntityType, Dao<?>> entityDaos = new EnumMap<>(EntityType.class);
private EntityDaoRegistry(List<Dao<?>> entityDaos, List<TenantEntityDao<?>> tenantEntityDaos) {
private EntityDaoRegistry(List<Dao<?>> entityDaos) {
entityDaos.forEach(dao -> {
if (dao.getEntityType() != null) {
this.entityDaos.put(dao.getEntityType(), dao);
}
});
tenantEntityDaos.forEach(dao -> {
if (dao.getType() != null) {
this.tenantEntityDaos.put(dao.getType(), dao);
}
});
}
public <T> Dao<T> getDao(EntityType entityType) {
@ -55,12 +47,4 @@ public class EntityDaoRegistry {
return dao;
}
public <T> TenantEntityDao<T> getTenantEntityDao(ObjectType objectType) {
TenantEntityDao<?> dao = tenantEntityDaos.get(objectType);
if (dao == null) {
throw new IllegalArgumentException("Missing tenant entity dao for entity type " + objectType);
}
return (TenantEntityDao<T>) dao;
}
}

65
dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java

@ -1,65 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.sql;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.AlarmType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.ToData;
import java.util.UUID;
@Data
@Entity
@Table(name = ModelConstants.ALARM_TYPES_TABLE_NAME)
@IdClass(AlarmTypeCompositeKey.class)
public class AlarmTypeEntity implements ToData<AlarmType> {
@Id
@Column(name = ModelConstants.TENANT_ID_PROPERTY, nullable = false)
private UUID tenantId;
@Id
@Column(name = ModelConstants.ALARM_TYPE_PROPERTY, nullable = false)
private String type;
public AlarmTypeEntity() {}
public AlarmTypeEntity(AlarmType alarmType) {
setTenantId(alarmType.getTenantId().getId());
setType(alarmType.getType());
}
public AlarmTypeEntity(UUID tenantId, String type) {
this.tenantId = tenantId;
this.type = type;
}
@Override
public AlarmType toData() {
AlarmType alarmType = new AlarmType();
alarmType.setTenantId(TenantId.fromUUID(tenantId));
alarmType.setType(type);
return alarmType;
}
}

30
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java

@ -1,30 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.alarm;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.AlarmTypeCompositeKey;
import org.thingsboard.server.dao.model.sql.AlarmTypeEntity;
import java.util.UUID;
public interface AlarmTypeRepository extends JpaRepository<AlarmTypeEntity, AlarmTypeCompositeKey> {
Page<AlarmTypeEntity> findByTenantId(UUID tenantId, Pageable pageable);
}

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

Loading…
Cancel
Save