Browse Source

Merge with master. Version set to 2.4.2

pull/2641/head
Igor Kulikov 7 years ago
parent
commit
19881d40a2
  1. 4
      .travis.yml
  2. 2
      application/pom.xml
  3. 6
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  4. 8
      application/src/main/data/json/system/widget_bundles/cards.json
  5. 6
      application/src/main/data/json/system/widget_bundles/charts.json
  6. 8
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  7. 10
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  8. 118
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  9. 31
      application/src/main/data/upgrade/2.4.2/schema_update.sql
  10. 24
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  11. 10
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  12. 35
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  13. 31
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  14. 2
      application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
  15. 4
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  16. 7
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  17. 16
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  18. 2
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  19. 3
      application/src/main/java/org/thingsboard/server/controller/EventController.java
  20. 6
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  21. 7
      application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
  22. 15
      application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
  23. 1
      application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java
  24. 1
      application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
  25. 1
      application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java
  26. 9
      application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
  27. 20
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  28. 2
      application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
  29. 2
      application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
  30. 83
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  31. 10
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java
  32. 6
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java
  33. 48
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  34. 5
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  35. 2
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  36. 253
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  37. 42
      application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java
  38. 19
      application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
  39. 83
      application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java
  40. 2
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  41. 2
      application/src/main/java/org/thingsboard/server/service/update/UpdateService.java
  42. 1
      application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
  43. 8
      application/src/main/proto/cluster.proto
  44. 9
      application/src/main/proto/jsinvoke.proto
  45. 74
      application/src/main/resources/thingsboard.yml
  46. 105
      application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java
  47. 2
      common/dao-api/pom.xml
  48. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java
  49. 0
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java
  50. 0
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java
  51. 0
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java
  52. 2
      common/data/pom.xml
  53. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java
  54. 2
      common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java
  55. 12
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  56. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
  57. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java
  58. 2
      common/message/pom.xml
  59. 18
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  60. 2
      common/pom.xml
  61. 2
      common/queue/pom.xml
  62. 14
      common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
  63. 11
      common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java
  64. 5
      common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java
  65. 2
      common/transport/coap/pom.xml
  66. 2
      common/transport/http/pom.xml
  67. 2
      common/transport/mqtt/pom.xml
  68. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  69. 2
      common/transport/pom.xml
  70. 2
      common/transport/transport-api/pom.xml
  71. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java
  72. 4
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  73. 33
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
  74. 2
      common/util/pom.xml
  75. 4
      common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java
  76. 6
      common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java
  77. 6
      dao/pom.xml
  78. 37
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  79. 45
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  80. 73
      dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
  81. 77
      dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java
  82. 88
      dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java
  83. 6
      dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
  84. 30
      dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java
  85. 55
      dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java
  86. 35
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  87. 43
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  88. 25
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  89. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  90. 8
      dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
  91. 17
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
  92. 8
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
  93. 15
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java
  94. 16
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java
  95. 53
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestEntity.java
  96. 29
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java
  97. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
  98. 35
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java
  99. 46
      dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java
  100. 116
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java

4
.travis.yml

@ -4,9 +4,9 @@ before_install:
- export MAVEN_OPTS="-Dmaven.repo.local=$HOME/.m2/repository -Xms1024m -Xmx3072m"
- export HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false
jdk:
- oraclejdk8
- openjdk8
language: java
sudo: required
services:
- docker
script: mvn clean verify -Ddockerfile.skip=false -DblackBoxTests.skip=false -DblackBoxTests.skipTailChildContainers=true
script: mvn clean verify -Ddockerfile.skip=false

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

6
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

@ -16,10 +16,10 @@
"templateHtml": "<tb-alarms-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStatusFilter\": {\n \"title\": \"Enable alarm status filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStatusFilter\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}"
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStatusFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}"
}
}
]
}
}

8
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

6
application/src/main/data/json/system/widget_bundles/charts.json

@ -35,7 +35,7 @@
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
@ -147,7 +147,7 @@
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
@ -163,7 +163,7 @@
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"

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

@ -109,9 +109,9 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" layout=\"column\">\n <div flex=\"20\" class=\"title-container\" layout=\"row\"\n layout-align=\"center center\" ng-show=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div flex=\"{{showTitle ? 80 : 100}}\" ng-style=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" layout=\"column\" layout-align=\"center center\">\n <div>\n <md-button ng-click=\"sendCommand()\" ng-class=\"{'md-raised': styleButton.isRaised, 'md-primary': styleButton.isPrimary}\" ng-style=\"customStyle\">\n {{buttonLable}}\n </md-button>\n </div>\n </div>\n <div class=\"error-container\" ng-style=\"{'background': vm.error.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n layout=\"row\" layout-align=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateHtml": "<div class=\"tb-rpc-button\" layout=\"column\">\n <div flex=\"20\" class=\"title-container\" layout=\"row\"\n layout-align=\"center center\" ng-show=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div flex=\"{{showTitle ? 80 : 100}}\" ng-style=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" layout=\"column\" layout-align=\"center center\">\n <div>\n <md-button ng-click=\"sendCommand()\" ng-class=\"{'md-raised': styleButton.isRaised, 'md-primary': styleButton.isPrimary}\" ng-style=\"customStyle\">\n {{buttonLabel}}\n </md-button>\n </div>\n </div>\n <div class=\"error-container\" ng-style=\"{'background': vm.error.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n layout=\"row\" layout-align=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};",
"controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Send RPC\"\n },\n \"oneWayElseTwoWay\": {\n \"title\": \"Is One Way Command\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showError\": {\n \"title\": \"Show RPC command execution error\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"methodName\": {\n \"title\": \"RPC method\",\n \"type\": \"string\",\n \"default\": \"rpcCommand\"\n },\n \"methodParams\": {\n \"title\": \"RPC method params\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 5000\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n \"oneWayElseTwoWay\",\n \"showError\",\n \"methodName\",\n {\n \"key\": \"methodParams\",\n \"type\": \"json\"\n },\n \"requestTimeout\",\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@ -125,9 +125,9 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" layout=\"column\">\n <div flex=\"20\" class=\"title-container\" layout=\"row\"\n layout-align=\"center center\" ng-show=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div flex=\"{{showTitle ? 80 : 100}}\" ng-style=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" layout=\"column\" layout-align=\"center center\">\n <div>\n <md-button ng-click=\"sendUpdate()\" ng-class=\"{'md-raised': styleButton.isRaised, 'md-primary': styleButton.isPrimary}\" ng-style=\"customStyle\">\n {{buttonLable}}\n </md-button>\n </div>\n </div>\n <div class=\"error-container\" ng-style=\"{'background': vm.error.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n layout=\"row\" layout-align=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateHtml": "<div class=\"tb-rpc-button\" layout=\"column\">\n <div flex=\"20\" class=\"title-container\" layout=\"row\"\n layout-align=\"center center\" ng-show=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div flex=\"{{showTitle ? 80 : 100}}\" ng-style=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" layout=\"column\" layout-align=\"center center\">\n <div>\n <md-button ng-click=\"sendUpdate()\" ng-class=\"{'md-raised': styleButton.isRaised, 'md-primary': styleButton.isPrimary}\" ng-style=\"customStyle\">\n {{buttonLabel}}\n </md-button>\n </div>\n </div>\n <div class=\"error-container\" ng-style=\"{'background': vm.error.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n layout=\"row\" layout-align=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Update device attribute\"\n },\n \"entityAttributeType\": {\n \"title\": \"Device attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"entityParameters\": {\n \"title\": \"Device attribute parameters\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n {\n \"key\": \"entityAttributeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n }, {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }]\n }, {\n \"key\": \"entityParameters\",\n \"type\": \"json\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}"

10
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

File diff suppressed because one or more lines are too long

118
application/src/main/data/json/system/widget_bundles/input_widgets.json

File diff suppressed because one or more lines are too long

31
application/src/main/data/upgrade/2.4.2/schema_update.sql

@ -0,0 +1,31 @@
--
-- Copyright © 2016-2019 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.
--
DROP INDEX IF EXISTS idx_alarm_originator_alarm_type;
CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC);
CREATE INDEX IF NOT EXISTS idx_device_customer_id ON device(tenant_id, customer_id);
CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id, customer_id, type);
CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type);
CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id);
CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);
CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type);

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

@ -32,6 +32,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
@ -88,6 +90,7 @@ import java.io.StringWriter;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
@ -285,6 +288,23 @@ public class ActorSystemContext {
@Getter
private long statisticsPersistFrequency;
@Getter
private final AtomicInteger jsInvokeRequestsCount = new AtomicInteger(0);
@Getter
private final AtomicInteger jsInvokeResponsesCount = new AtomicInteger(0);
@Getter
private final AtomicInteger jsInvokeFailuresCount = new AtomicInteger(0);
@Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}")
public void printStats() {
if (statisticsEnabled) {
if (jsInvokeRequestsCount.get() > 0 || jsInvokeResponsesCount.get() > 0 || jsInvokeFailuresCount.get() > 0) {
log.info("Rule Engine JS Invoke Stats: requests [{}] responses [{}] failures [{}]",
jsInvokeRequestsCount.getAndSet(0), jsInvokeResponsesCount.getAndSet(0), jsInvokeFailuresCount.getAndSet(0));
}
}
}
@Value("${actors.tenant.create_components_on_init}")
@Getter
private boolean tenantComponentsInitEnabled;
@ -336,6 +356,10 @@ public class ActorSystemContext {
@Getter
private CassandraBufferedRateExecutor cassandraBufferedRateExecutor;
@Autowired(required = false)
@Getter
private RedisTemplate<String, Object> redisTemplate;
public ActorSystemContext() {
config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
}

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

@ -223,6 +223,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
void process(ActorContext context, TransportToDeviceActorMsgWrapper wrapper) {
boolean reportDeviceActivity = false;
TransportToDeviceActorMsg msg = wrapper.getMsg();
if (msg.hasSessionEvent()) {
processSessionStateMsgs(msg.getSessionInfo(), msg.getSessionEvent());
@ -235,11 +236,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
if (msg.hasPostAttributes()) {
handlePostAttributesRequest(context, msg.getSessionInfo(), msg.getPostAttributes());
reportLogicalDeviceActivity();
reportDeviceActivity = true;
}
if (msg.hasPostTelemetry()) {
handlePostTelemetryRequest(context, msg.getSessionInfo(), msg.getPostTelemetry());
reportLogicalDeviceActivity();
reportDeviceActivity = true;
}
if (msg.hasGetAttributes()) {
handleGetAttributesRequest(context, msg.getSessionInfo(), msg.getGetAttributes());
@ -249,11 +250,14 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
if (msg.hasToServerRPCCallRequest()) {
handleClientSideRPCRequest(context, msg.getSessionInfo(), msg.getToServerRPCCallRequest());
reportLogicalDeviceActivity();
reportDeviceActivity = true;
}
if (msg.hasSubscriptionInfo()) {
handleSessionActivity(context, msg.getSessionInfo(), msg.getSubscriptionInfo());
}
if (reportDeviceActivity) {
reportLogicalDeviceActivity();
}
}
private void reportLogicalDeviceActivity() {

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

@ -22,8 +22,9 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.channel.EventLoopGroup;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.ListeningExecutor;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
@ -60,7 +61,6 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -229,6 +229,27 @@ class DefaultTbContext implements TbContext {
return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
}
@Override
public void logJsEvalRequest() {
if (mainCtx.isStatisticsEnabled()) {
mainCtx.getJsInvokeRequestsCount().incrementAndGet();
}
}
@Override
public void logJsEvalResponse() {
if (mainCtx.isStatisticsEnabled()) {
mainCtx.getJsInvokeResponsesCount().incrementAndGet();
}
}
@Override
public void logJsEvalFailure() {
if (mainCtx.isStatisticsEnabled()) {
mainCtx.getJsInvokeFailuresCount().incrementAndGet();
}
}
@Override
public String getNodeId() {
return mainCtx.getNodeIdProvider().getNodeId();
@ -361,6 +382,16 @@ class DefaultTbContext implements TbContext {
return mainCtx.getCassandraBufferedRateExecutor().submit(task);
}
@Override
public RedisTemplate<String, Object> getRedisTemplate() {
return mainCtx.getRedisTemplate();
}
@Override
public String getServerAddress() {
return mainCtx.getServerAddress();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());

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

@ -22,8 +22,10 @@ import akka.actor.Terminated;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
@ -50,6 +52,7 @@ import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.transport.RuleEngineStats;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;
@ -58,6 +61,7 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
@ -187,8 +191,29 @@ public class DefaultActorService implements ActorService {
this.rpcManagerActor.tell(msg, ActorRef.noSender());
}
@Value("${cluster.stats.enabled:false}")
private boolean statsEnabled;
private final AtomicInteger sentClusterMsgs = new AtomicInteger(0);
private final AtomicInteger receivedClusterMsgs = new AtomicInteger(0);
@Scheduled(fixedDelayString = "${cluster.stats.print_interval_ms}")
public void printStats() {
if (statsEnabled) {
int sent = sentClusterMsgs.getAndSet(0);
int received = receivedClusterMsgs.getAndSet(0);
if (sent > 0 || received > 0) {
log.info("Cluster msgs sent [{}] received [{}]", sent, received);
}
}
}
@Override
public void onReceivedMsg(ServerAddress source, ClusterAPIProtos.ClusterMessage msg) {
if (statsEnabled) {
receivedClusterMsgs.incrementAndGet();
}
ServerAddress serverAddress = new ServerAddress(source.getHost(), source.getPort(), source.getServerType());
if (log.isDebugEnabled()) {
log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress);
@ -239,11 +264,17 @@ public class DefaultActorService implements ActorService {
@Override
public void onSendMsg(ClusterAPIProtos.ClusterMessage msg) {
if (statsEnabled) {
sentClusterMsgs.incrementAndGet();
}
rpcManagerActor.tell(msg, ActorRef.noSender());
}
@Override
public void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg) {
if (statsEnabled) {
sentClusterMsgs.incrementAndGet();
}
rpcManagerActor.tell(msg, ActorRef.noSender());
}

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

@ -84,7 +84,7 @@ public class ThingsboardMessageConfiguration {
}
@Slf4j
static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader {
public static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader {
public static final String NAME = "spring";

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

@ -28,12 +28,12 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.update.UpdateService;
import org.thingsboard.server.service.update.model.UpdateMessage;
import org.thingsboard.server.common.data.UpdateMessage;
@RestController
@RequestMapping("/api/admin")

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

@ -24,7 +24,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -43,14 +42,12 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import ua_parser.Client;

16
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -516,15 +516,27 @@ public abstract class BaseController {
protected String constructBaseUrl(HttpServletRequest request) {
String scheme = request.getScheme();
if (request.getHeader("x-forwarded-proto") != null) {
scheme = request.getHeader("x-forwarded-proto");
String forwardedProto = request.getHeader("x-forwarded-proto");
if (forwardedProto != null) {
scheme = forwardedProto;
}
int serverPort = request.getServerPort();
if (request.getHeader("x-forwarded-port") != null) {
try {
serverPort = request.getIntHeader("x-forwarded-port");
} catch (NumberFormatException e) {
}
} else if (forwardedProto != null) {
switch (forwardedProto) {
case "http":
serverPort = 80;
break;
case "https":
serverPort = 443;
break;
}
}
String baseUrl = String.format("%s://%s:%d",

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

@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.controller.claim.data.ClaimRequest;
import org.thingsboard.server.common.data.ClaimRequest;
import org.thingsboard.server.dao.device.claim.ClaimResponse;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import org.thingsboard.server.dao.exception.IncorrectParameterException;

3
application/src/main/java/org/thingsboard/server/controller/EventController.java

@ -24,7 +24,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -32,9 +31,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@RestController
@RequestMapping("/api")

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

@ -119,6 +119,11 @@ public class ThingsboardInstallService {
case "2.4.0":
log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ...");
case "2.4.1":
log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ...");
databaseUpgradeService.upgradeDatabase("2.4.1");
log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts");
@ -133,6 +138,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("input_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("date");
systemDataLoaderService.deleteSystemWidgetBundle("entity_admin_widgets");
systemDataLoaderService.loadSystemWidgets();
break;

7
application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java

@ -30,13 +30,6 @@ public interface ClusterRoutingService extends DiscoveryServiceListener {
ServerAddress getCurrentServer();
Optional<ServerAddress> resolveByUuid(UUID uuid);
Optional<ServerAddress> resolveById(EntityId entityId);
Optional<ServerAddress> resolveByUuid(ServerType server, UUID uuid);
Optional<ServerAddress> resolveById(ServerType server, EntityId entityId);
}

15
application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java

@ -88,21 +88,6 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService {
return resolveByUuid(rootCircle, entityId.getId());
}
@Override
public Optional<ServerAddress> resolveByUuid(UUID uuid) {
return resolveByUuid(rootCircle, uuid);
}
@Override
public Optional<ServerAddress> resolveByUuid(ServerType server, UUID uuid) {
return resolveByUuid(circles[server.ordinal()], uuid);
}
@Override
public Optional<ServerAddress> resolveById(ServerType server, EntityId entityId) {
return resolveByUuid(circles[server.ordinal()], entityId.getId());
}
private Optional<ServerAddress> resolveByUuid(ConsistentHashCircle circle, UUID uuid) {
Assert.notNull(uuid);
if (circle.isEmpty()) {

1
application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class ClusterRpcCallbackExecutorService extends AbstractListeningExecutor {

1
application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class DbCallbackExecutorService extends AbstractListeningExecutor {

1
application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class ExternalCallExecutorService extends AbstractListeningExecutor {

9
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java

@ -267,6 +267,15 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
} catch (InvalidQueryException e) {}
log.info("Schema updated.");
break;
case "2.4.1":
log.info("Updating schema ...");
String updateAssetTableStmt = "alter table asset add label text";
try {
cluster.getSession().execute(updateAssetTableStmt);
Thread.sleep(2500);
} catch (InvalidQueryException e) {}
log.info("Schema updated.");
break;
default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
}

20
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -176,6 +176,26 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Schema updated.");
}
break;
case "2.4.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
try {
conn.createStatement().execute("ALTER TABLE asset ADD COLUMN label varchar(255)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {}
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.2", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
try {
conn.createStatement().execute("ALTER TABLE device ADD CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {}
try {
conn.createStatement().execute("ALTER TABLE device_credentials ADD CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {}
try {
conn.createStatement().execute("ALTER TABLE asset ADD CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {}
log.info("Schema updated.");
}
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}

2
application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java

@ -17,7 +17,7 @@ package org.thingsboard.server.service.mail;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.executors.AbstractListeningExecutor;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class MailExecutorService extends AbstractListeningExecutor {

2
application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java

@ -17,7 +17,7 @@ package org.thingsboard.server.service.script;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.executors.AbstractListeningExecutor;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class JsExecutorService extends AbstractListeningExecutor {

83
application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
@ -22,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.kafka.TBKafkaConsumerTemplate;
@ -29,13 +31,16 @@ import org.thingsboard.server.kafka.TBKafkaProducerTemplate;
import org.thingsboard.server.kafka.TbKafkaRequestTemplate;
import org.thingsboard.server.kafka.TbKafkaSettings;
import org.thingsboard.server.kafka.TbNodeIdProvider;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "remote", matchIfMissing = true)
@ -70,6 +75,30 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Value("${js.remote.max_errors}")
private int maxErrors;
@Value("${js.remote.stats.enabled:false}")
private boolean statsEnabled;
private final AtomicInteger kafkaPushedMsgs = new AtomicInteger(0);
private final AtomicInteger kafkaInvokeMsgs = new AtomicInteger(0);
private final AtomicInteger kafkaEvalMsgs = new AtomicInteger(0);
private final AtomicInteger kafkaFailedMsgs = new AtomicInteger(0);
private final AtomicInteger kafkaTimeoutMsgs = new AtomicInteger(0);
@Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}")
public void printStats() {
if (statsEnabled) {
int pushedMsgs = kafkaPushedMsgs.getAndSet(0);
int invokeMsgs = kafkaInvokeMsgs.getAndSet(0);
int evalMsgs = kafkaEvalMsgs.getAndSet(0);
int failed = kafkaFailedMsgs.getAndSet(0);
int timedOut = kafkaTimeoutMsgs.getAndSet(0);
if (pushedMsgs > 0 || invokeMsgs > 0 || evalMsgs > 0 || failed > 0 || timedOut > 0) {
log.info("Kafka JS Invoke Stats: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]",
pushedMsgs, invokeMsgs + evalMsgs, invokeMsgs, evalMsgs, failed, timedOut);
}
}
}
private TbKafkaRequestTemplate<JsInvokeProtos.RemoteJsRequest, JsInvokeProtos.RemoteJsResponse> kafkaTemplate;
private Map<UUID, String> scriptIdToBodysMap = new ConcurrentHashMap<>();
@ -80,22 +109,6 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
requestBuilder.clientId("producer-js-invoke-" + nodeIdProvider.getNodeId());
requestBuilder.defaultTopic(requestTopic);
requestBuilder.encoder(new RemoteJsRequestEncoder());
requestBuilder.enricher((request, responseTopic, requestId) -> {
JsInvokeProtos.RemoteJsRequest.Builder remoteRequest = JsInvokeProtos.RemoteJsRequest.newBuilder();
if (request.hasCompileRequest()) {
remoteRequest.setCompileRequest(request.getCompileRequest());
}
if (request.hasInvokeRequest()) {
remoteRequest.setInvokeRequest(request.getInvokeRequest());
}
if (request.hasReleaseRequest()) {
remoteRequest.setReleaseRequest(request.getReleaseRequest());
}
remoteRequest.setResponseTopic(responseTopic);
remoteRequest.setRequestIdMSB(requestId.getMostSignificantBits());
remoteRequest.setRequestIdLSB(requestId.getLeastSignificantBits());
return remoteRequest.build();
});
TBKafkaConsumerTemplate.TBKafkaConsumerTemplateBuilder<JsInvokeProtos.RemoteJsResponse> responseBuilder = TBKafkaConsumerTemplate.builder();
responseBuilder.settings(kafkaSettings);
@ -138,7 +151,22 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.build();
log.trace("Post compile request for scriptId [{}]", scriptId);
ListenableFuture<JsInvokeProtos.RemoteJsResponse> future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper);
ListenableFuture<JsInvokeProtos.RemoteJsResponse> future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper);
kafkaPushedMsgs.incrementAndGet();
Futures.addCallback(future, new FutureCallback<JsInvokeProtos.RemoteJsResponse>() {
@Override
public void onSuccess(@Nullable JsInvokeProtos.RemoteJsResponse result) {
kafkaEvalMsgs.incrementAndGet();
}
@Override
public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
kafkaTimeoutMsgs.incrementAndGet();
}
kafkaFailedMsgs.incrementAndGet();
}
});
return Futures.transform(future, response -> {
JsInvokeProtos.JsCompileResponse compilationResult = response.getCompileResponse();
UUID compiledScriptId = new UUID(compilationResult.getScriptIdMSB(), compilationResult.getScriptIdLSB());
@ -174,7 +202,22 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setInvokeRequest(jsRequestBuilder.build())
.build();
ListenableFuture<JsInvokeProtos.RemoteJsResponse> future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper);
ListenableFuture<JsInvokeProtos.RemoteJsResponse> future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper);
kafkaPushedMsgs.incrementAndGet();
Futures.addCallback(future, new FutureCallback<JsInvokeProtos.RemoteJsResponse>() {
@Override
public void onSuccess(@Nullable JsInvokeProtos.RemoteJsResponse result) {
kafkaInvokeMsgs.incrementAndGet();
}
@Override
public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
kafkaTimeoutMsgs.incrementAndGet();
}
kafkaFailedMsgs.incrementAndGet();
}
});
return Futures.transform(future, response -> {
JsInvokeProtos.JsInvokeResponse invokeResult = response.getInvokeResponse();
if (invokeResult.getSuccess()) {
@ -197,7 +240,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setReleaseRequest(jsRequest)
.build();
ListenableFuture<JsInvokeProtos.RemoteJsResponse> future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper);
ListenableFuture<JsInvokeProtos.RemoteJsResponse> future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper);
JsInvokeProtos.RemoteJsResponse response = future.get();
JsInvokeProtos.JsReleaseResponse compilationResult = response.getReleaseResponse();

10
application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java

@ -15,15 +15,23 @@
*/
package org.thingsboard.server.service.script;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.kafka.TbKafkaEncoder;
import java.nio.charset.StandardCharsets;
/**
* Created by ashvayka on 25.09.18.
*/
public class RemoteJsRequestEncoder implements TbKafkaEncoder<JsInvokeProtos.RemoteJsRequest> {
@Override
public byte[] encode(JsInvokeProtos.RemoteJsRequest value) {
return value.toByteArray();
try {
return JsonFormat.printer().print(value).getBytes(StandardCharsets.UTF_8);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}
}

6
application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java

@ -15,10 +15,12 @@
*/
package org.thingsboard.server.service.script;
import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.kafka.TbKafkaDecoder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Created by ashvayka on 25.09.18.
@ -27,6 +29,8 @@ public class RemoteJsResponseDecoder implements TbKafkaDecoder<JsInvokeProtos.Re
@Override
public JsInvokeProtos.RemoteJsResponse decode(byte[] data) throws IOException {
return JsInvokeProtos.RemoteJsResponse.parseFrom(data);
JsInvokeProtos.RemoteJsResponse.Builder builder = JsInvokeProtos.RemoteJsResponse.newBuilder();
JsonFormat.parser().ignoringUnknownFields().merge(new String(data, StandardCharsets.UTF_8), builder);
return builder.build();
}
}

48
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -19,6 +19,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
@ -109,6 +111,19 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return unbindMsg(result, msg);
}
@Override
public ListenableFuture<TbMsg> executeUpdateAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result, json -> {
if (!json.isObject()) {
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} else {
return Futures.immediateFuture(unbindMsg(json, msg));
}
});
}
@Override
public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
JsonNode result = executeScript(prevMsg);
@ -144,6 +159,19 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return result.asBoolean();
}
@Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result, json -> {
if (!json.isBoolean()) {
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} else {
return Futures.immediateFuture(json.asBoolean());
}
});
}
@Override
public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
@ -173,7 +201,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return mapper.readTree(eval);
} catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) {
throw (ScriptException)e.getCause();
throw (ScriptException) e.getCause();
} else if (e.getCause() instanceof RuntimeException) {
throw new ScriptException(e.getCause().getMessage());
} else {
@ -184,6 +212,24 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
}
}
private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
String[] inArgs = prepareArgs(msg);
return Futures.transformAsync(sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o.toString()));
} catch (Exception e) {
if (e.getCause() instanceof ScriptException) {
return Futures.immediateFailedFuture(e.getCause());
} else if (e.getCause() instanceof RuntimeException) {
return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
} else {
return Futures.immediateFailedFuture(new ScriptException(e));
}
}
});
}
public void destroy() {
sandboxService.release(this.scriptId);
}

5
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -42,14 +42,13 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.service.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import javax.annotation.Resource;
import java.util.ArrayList;

2
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -19,7 +19,7 @@ import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.service.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
public interface SystemSecurityService {

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

@ -30,7 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RpcError;
import org.springframework.util.StringUtils;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@ -38,8 +38,13 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -49,22 +54,16 @@ import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
@ -92,7 +91,8 @@ public class DefaultDeviceStateService implements DeviceStateService {
public static final String INACTIVITY_ALARM_TIME = "inactivityAlarmTime";
public static final String INACTIVITY_TIMEOUT = "inactivityTimeout";
public static final List<String> PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
public static final List<String> PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME,
LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
@Autowired
private TenantService tenantService;
@ -103,6 +103,9 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Autowired
private AttributesService attributesService;
@Autowired
private TimeseriesService tsService;
@Autowired
@Lazy
private ActorService actorService;
@ -122,19 +125,19 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Value("${state.defaultStateCheckIntervalInSec}")
@Getter
private long defaultStateCheckIntervalInSec;
private int defaultStateCheckIntervalInSec;
// TODO in v2.1
// @Value("${state.defaultStatePersistenceIntervalInSec}")
// @Getter
// private long defaultStatePersistenceIntervalInSec;
//
// @Value("${state.defaultStatePersistencePack}")
// @Getter
// private long defaultStatePersistencePack;
@Value("${state.persistToTelemetry:false}")
@Getter
private boolean persistToTelemetry;
private ListeningScheduledExecutorService queueExecutor;
@Value("${state.initFetchPackSize:1000}")
@Getter
private int initFetchPackSize;
private volatile boolean clusterUpdatePending = false;
private ListeningScheduledExecutorService queueExecutor;
private ConcurrentMap<TenantId, Set<DeviceId>> tenantDevices = new ConcurrentHashMap<>();
private ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
@ -143,7 +146,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
// Should be always single threaded due to absence of locks.
queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor());
queueExecutor.submit(this::initStateFromDB);
queueExecutor.scheduleAtFixedRate(this::updateState, defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
//TODO: schedule persistence in v2.1;
}
@ -191,7 +194,10 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Override
public void onClusterUpdate() {
queueExecutor.submit(this::onClusterUpdateSync);
if (!clusterUpdatePending) {
clusterUpdatePending = true;
queueExecutor.submit(this::onClusterUpdateSync);
}
}
@Override
@ -219,46 +225,59 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
private void onClusterUpdateSync() {
clusterUpdatePending = false;
List<Tenant> tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
for (Tenant tenant : tenants) {
List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
for (Device device : devices) {
if (!routingService.resolveById(device.getId()).isPresent()) {
if (!deviceStates.containsKey(device.getId())) {
fetchFutures.add(fetchDeviceState(device));
TextPageLink pageLink = new TextPageLink(initFetchPackSize);
while (pageLink != null) {
TextPageData<Device> page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink);
pageLink = page.getNextPageLink();
for (Device device : page.getData()) {
if (!routingService.resolveById(device.getId()).isPresent()) {
if (!deviceStates.containsKey(device.getId())) {
fetchFutures.add(fetchDeviceState(device));
}
} else {
Set<DeviceId> tenantDeviceSet = tenantDevices.get(tenant.getId());
if (tenantDeviceSet != null) {
tenantDeviceSet.remove(device.getId());
}
deviceStates.remove(device.getId());
}
} else {
Set<DeviceId> tenantDeviceSet = tenantDevices.get(tenant.getId());
if (tenantDeviceSet != null) {
tenantDeviceSet.remove(device.getId());
}
deviceStates.remove(device.getId());
}
}
try {
Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to init device state service from DB", e);
try {
Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to init device state service from DB", e);
}
}
}
}
private void initStateFromDB() {
List<Tenant> tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
for (Tenant tenant : tenants) {
List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
for (Device device : devices) {
if (!routingService.resolveById(device.getId()).isPresent()) {
fetchFutures.add(fetchDeviceState(device));
try {
List<Tenant> tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
for (Tenant tenant : tenants) {
List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
TextPageLink pageLink = new TextPageLink(initFetchPackSize);
while (pageLink != null) {
TextPageData<Device> page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink);
pageLink = page.getNextPageLink();
for (Device device : page.getData()) {
if (!routingService.resolveById(device.getId()).isPresent()) {
fetchFutures.add(fetchDeviceState(device));
}
}
try {
Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to init device state service from DB", e);
}
}
}
try {
Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to init device state service from DB", e);
}
} catch (Throwable t) {
log.warn("Failed to init device states from DB", t);
}
}
@ -270,15 +289,21 @@ public class DefaultDeviceStateService implements DeviceStateService {
private void updateState() {
long ts = System.currentTimeMillis();
Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet());
log.info("Calculating state updates for {} devices", deviceStates.size());
for (DeviceId deviceId : deviceIds) {
DeviceStateData stateData = deviceStates.get(deviceId);
DeviceState state = stateData.getState();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) {
state.setLastInactivityAlarmTime(ts);
pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts);
saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
if (stateData != null) {
DeviceState state = stateData.getState();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) {
state.setLastInactivityAlarmTime(ts);
pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
save(deviceId, INACTIVITY_ALARM_TIME, ts);
save(deviceId, ACTIVITY_STATE, state.isActive());
}
} else {
log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId);
deviceStates.remove(deviceId);
}
}
}
@ -289,7 +314,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
long ts = System.currentTimeMillis();
stateData.getState().setLastConnectTime(ts);
pushRuleEngineMessage(stateData, CONNECT_EVENT);
saveAttribute(deviceId, LAST_CONNECT_TIME, ts);
save(deviceId, LAST_CONNECT_TIME, ts);
}
}
@ -299,7 +324,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
long ts = System.currentTimeMillis();
stateData.getState().setLastDisconnectTime(ts);
pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
saveAttribute(deviceId, LAST_DISCONNECT_TIME, ts);
save(deviceId, LAST_DISCONNECT_TIME, ts);
}
}
@ -308,11 +333,14 @@ public class DefaultDeviceStateService implements DeviceStateService {
if (stateData != null) {
DeviceState state = stateData.getState();
long ts = System.currentTimeMillis();
state.setActive(true);
stateData.getState().setLastActivityTime(ts);
pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
saveAttribute(deviceId, LAST_ACTIVITY_TIME, ts);
saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
save(deviceId, LAST_ACTIVITY_TIME, ts);
if (!state.isActive()) {
state.setActive(true);
save(deviceId, ACTIVITY_STATE, state.isActive());
}
}
}
@ -324,6 +352,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
if (device != null) {
try {
deviceStateData = fetchDeviceState(device).get();
deviceStates.putIfAbsent(deviceId, deviceStateData);
} catch (InterruptedException | ExecutionException e) {
log.debug("[{}] Failed to fetch device state!", deviceId, e);
}
@ -345,7 +374,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
boolean oldActive = state.isActive();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!oldActive && state.isActive() || oldActive && !state.isActive()) {
saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
save(deviceId, ACTIVITY_STATE, state.isActive());
}
}
}
@ -414,39 +443,55 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) {
ListenableFuture<List<AttributeKvEntry>> attributes = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
return Futures.transform(attributes, new Function<List<AttributeKvEntry>, DeviceStateData>() {
if (persistToTelemetry) {
ListenableFuture<List<TsKvEntry>> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES);
return Futures.transform(tsData, extractDeviceStateData(device));
} else {
ListenableFuture<List<AttributeKvEntry>> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
return Futures.transform(attrData, extractDeviceStateData(device));
}
}
private <T extends KvEntry> Function<List<T>, DeviceStateData> extractDeviceStateData(Device device) {
return new Function<List<T>, DeviceStateData>() {
@Nullable
@Override
public DeviceStateData apply(@Nullable List<AttributeKvEntry> attributes) {
long lastActivityTime = getAttributeValue(attributes, LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getAttributeValue(attributes, INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getAttributeValue(attributes, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout;
DeviceState deviceState = DeviceState.builder()
.active(active)
.lastConnectTime(getAttributeValue(attributes, LAST_CONNECT_TIME, 0L))
.lastDisconnectTime(getAttributeValue(attributes, LAST_DISCONNECT_TIME, 0L))
.lastActivityTime(lastActivityTime)
.lastInactivityAlarmTime(inactivityAlarmTime)
.inactivityTimeout(inactivityTimeout)
.build();
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName());
md.putValue("deviceType", device.getType());
return DeviceStateData.builder()
.tenantId(device.getTenantId())
.deviceId(device.getId())
.metaData(md)
.state(deviceState).build();
public DeviceStateData apply(@Nullable List<T> data) {
try {
long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout;
DeviceState deviceState = DeviceState.builder()
.active(active)
.lastConnectTime(getEntryValue(data, LAST_CONNECT_TIME, 0L))
.lastDisconnectTime(getEntryValue(data, LAST_DISCONNECT_TIME, 0L))
.lastActivityTime(lastActivityTime)
.lastInactivityAlarmTime(inactivityAlarmTime)
.inactivityTimeout(inactivityTimeout)
.build();
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName());
md.putValue("deviceType", device.getType());
return DeviceStateData.builder()
.tenantId(device.getTenantId())
.deviceId(device.getId())
.metaData(md)
.state(deviceState).build();
} catch (Exception e) {
log.warn("[{}] Failed to fetch device state data", device.getId(), e);
throw new RuntimeException(e);
}
}
});
};
}
private long getAttributeValue(List<AttributeKvEntry> attributes, String attributeName, long defaultValue) {
for (AttributeKvEntry attribute : attributes) {
if (attribute.getKey().equals(attributeName)) {
return attribute.getLongValue().orElse(defaultValue);
private long getEntryValue(List<? extends KvEntry> kvEntries, String attributeName, long defaultValue) {
if (kvEntries != null) {
for (KvEntry entry : kvEntries) {
if (entry != null && !StringUtils.isEmpty(entry.getKey()) && entry.getKey().equals(attributeName)) {
return entry.getLongValue().orElse(defaultValue);
}
}
}
return defaultValue;
@ -464,12 +509,26 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
}
private void saveAttribute(DeviceId deviceId, String key, long value) {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
private void save(DeviceId deviceId, String key, long value) {
if (persistToTelemetry) {
tsSubService.saveAndNotify(
TenantId.SYS_TENANT_ID, deviceId,
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))),
new AttributeSaveCallback(deviceId, key, value));
} else {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
}
}
private void saveAttribute(DeviceId deviceId, String key, boolean value) {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
private void save(DeviceId deviceId, String key, boolean value) {
if (persistToTelemetry) {
tsSubService.saveAndNotify(
TenantId.SYS_TENANT_ID, deviceId,
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))),
new AttributeSaveCallback(deviceId, key, value));
} else {
tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
}
}
private class AttributeSaveCallback implements FutureCallback<Void> {

42
application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java

@ -15,14 +15,12 @@
*/
package org.thingsboard.server.service.transaction;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
@ -34,7 +32,6 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Optional;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
@ -111,29 +108,18 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ
@Override
public void endTransaction(TbMsg msg, Consumer<TbMsg> onSuccess, Consumer<Throwable> onFailure) {
EntityId originatorId = msg.getTransactionData().getOriginatorId();
UUID transactionId = msg.getTransactionData().getTransactionId();
Optional<ServerAddress> address = routingService.resolveById(originatorId);
Optional<ServerAddress> address = routingService.resolveById(msg.getTransactionData().getOriginatorId());
if (address.isPresent()) {
sendTransactionEventToRemoteServer(originatorId, transactionId, address.get());
sendTransactionEventToRemoteServer(msg, address.get());
executeOnSuccess(onSuccess, msg);
} else {
endLocalTransaction(transactionId, originatorId, onSuccess, onFailure);
endLocalTransaction(msg, onSuccess, onFailure);
}
}
@Override
public void onRemoteTransactionMsg(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.TransactionEndServiceMsgProto proto;
try {
proto = ClusterAPIProtos.TransactionEndServiceMsgProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
EntityId originatorId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getOriginatorIdMSB(), proto.getOriginatorIdLSB()));
UUID transactionId = new UUID(proto.getTransactionIdMSB(), proto.getTransactionIdLSB());
endLocalTransaction(transactionId, originatorId, msg -> {
endLocalTransaction(TbMsg.fromBytes(data), msg -> {
}, error -> {
});
}
@ -144,21 +130,21 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ
log.trace("Added msg to queue, size: [{}]", queue.size());
}
private void endLocalTransaction(UUID transactionId, EntityId originatorId, Consumer<TbMsg> onSuccess, Consumer<Throwable> onFailure) {
private void endLocalTransaction(TbMsg msg, Consumer<TbMsg> onSuccess, Consumer<Throwable> onFailure) {
transactionLock.lock();
try {
BlockingQueue<TbTransactionTask> queue = transactionMap.computeIfAbsent(originatorId, id ->
BlockingQueue<TbTransactionTask> queue = transactionMap.computeIfAbsent(msg.getTransactionData().getOriginatorId(), id ->
new LinkedBlockingQueue<>(finalQueueSize));
TbTransactionTask currentTransactionTask = queue.peek();
if (currentTransactionTask != null) {
if (currentTransactionTask.getMsg().getTransactionData().getTransactionId().equals(transactionId)) {
if (currentTransactionTask.getMsg().getTransactionData().getTransactionId().equals(msg.getTransactionData().getTransactionId())) {
currentTransactionTask.setCompleted(true);
queue.poll();
log.trace("Removed msg from queue, size [{}]", queue.size());
executeOnSuccess(currentTransactionTask.getOnEnd(), currentTransactionTask.getMsg());
executeOnSuccess(onSuccess, currentTransactionTask.getMsg());
executeOnSuccess(onSuccess, msg);
TbTransactionTask nextTransactionTask = queue.peek();
if (nextTransactionTask != null) {
@ -247,14 +233,8 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ
callbackExecutor.executeAsync(task);
}
private void sendTransactionEventToRemoteServer(EntityId entityId, UUID transactionId, ServerAddress address) {
log.trace("[{}][{}] Originator is monitored on other server: {}", entityId, transactionId, address);
ClusterAPIProtos.TransactionEndServiceMsgProto.Builder builder = ClusterAPIProtos.TransactionEndServiceMsgProto.newBuilder();
builder.setEntityType(entityId.getEntityType().name());
builder.setOriginatorIdMSB(entityId.getId().getMostSignificantBits());
builder.setOriginatorIdLSB(entityId.getId().getLeastSignificantBits());
builder.setTransactionIdMSB(transactionId.getMostSignificantBits());
builder.setTransactionIdLSB(transactionId.getLeastSignificantBits());
clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TRANSACTION_SERVICE_MESSAGE, builder.build().toByteArray());
private void sendTransactionEventToRemoteServer(TbMsg msg, ServerAddress address) {
log.trace("[{}][{}] Originator is monitored on other server: {}", msg.getTransactionData().getOriginatorId(), msg.getTransactionData().getTransactionId(), address);
clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TRANSACTION_SERVICE_MESSAGE, TbMsg.toByteArray(msg));
}
}

19
application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.transport;
import akka.actor.ActorRef;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.BlockingBucket;
import io.github.bucket4j.Bucket4j;
@ -30,11 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceActorToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
@ -44,7 +42,6 @@ import org.thingsboard.server.kafka.TBKafkaConsumerTemplate;
import org.thingsboard.server.kafka.TBKafkaProducerTemplate;
import org.thingsboard.server.kafka.TbKafkaSettings;
import org.thingsboard.server.kafka.TbNodeIdProvider;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
@ -83,6 +80,8 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
private long pollRecordsPerSecond;
@Value("${transport.remote.rule_engine.max_poll_records_per_minute}")
private long pollRecordsPerMinute;
@Value("${transport.remote.rule_engine.stats.enabled:false}")
private boolean statsEnabled;
@Autowired
private TbKafkaSettings kafkaSettings;
@ -108,6 +107,8 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
private volatile boolean stopped = false;
private final RuleEngineStats stats = new RuleEngineStats();
@PostConstruct
public void init() {
TBKafkaProducerTemplate.TBKafkaProducerTemplateBuilder<ToTransportMsg> notificationsProducerBuilder = TBKafkaProducerTemplate.builder();
@ -176,6 +177,13 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
});
}
@Scheduled(fixedDelayString = "${transport.remote.rule_engine.stats.print_interval_ms}")
public void printStats() {
if (statsEnabled) {
stats.printStats();
}
}
@Override
public void process(String nodeId, DeviceActorToTransportMsg msg) {
process(nodeId, msg, null, null);
@ -191,6 +199,9 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
}
private void forwardToDeviceActor(TransportToDeviceActorMsg toDeviceActorMsg) {
if (statsEnabled) {
stats.log(toDeviceActorMsg);
}
TransportToDeviceActorMsgWrapper wrapper = new TransportToDeviceActorMsgWrapper(toDeviceActorMsg);
Optional<ServerAddress> address = routingService.resolveById(wrapper.getDeviceId());
if (address.isPresent()) {

83
application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2019 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.transport;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class RuleEngineStats {
private final AtomicInteger totalCounter = new AtomicInteger(0);
private final AtomicInteger sessionEventCounter = new AtomicInteger(0);
private final AtomicInteger postTelemetryCounter = new AtomicInteger(0);
private final AtomicInteger postAttributesCounter = new AtomicInteger(0);
private final AtomicInteger getAttributesCounter = new AtomicInteger(0);
private final AtomicInteger subscribeToAttributesCounter = new AtomicInteger(0);
private final AtomicInteger subscribeToRPCCounter = new AtomicInteger(0);
private final AtomicInteger toDeviceRPCCallResponseCounter = new AtomicInteger(0);
private final AtomicInteger toServerRPCCallRequestCounter = new AtomicInteger(0);
private final AtomicInteger subscriptionInfoCounter = new AtomicInteger(0);
private final AtomicInteger claimDeviceCounter = new AtomicInteger(0);
public void log(TransportProtos.TransportToDeviceActorMsg msg) {
totalCounter.incrementAndGet();
if (msg.hasSessionEvent()) {
sessionEventCounter.incrementAndGet();
}
if (msg.hasPostTelemetry()) {
postTelemetryCounter.incrementAndGet();
}
if (msg.hasPostAttributes()) {
postAttributesCounter.incrementAndGet();
}
if (msg.hasGetAttributes()) {
getAttributesCounter.incrementAndGet();
}
if (msg.hasSubscribeToAttributes()) {
subscribeToAttributesCounter.incrementAndGet();
}
if (msg.hasSubscribeToRPC()) {
subscribeToRPCCounter.incrementAndGet();
}
if (msg.hasToDeviceRPCCallResponse()) {
toDeviceRPCCallResponseCounter.incrementAndGet();
}
if (msg.hasToServerRPCCallRequest()) {
toServerRPCCallRequestCounter.incrementAndGet();
}
if (msg.hasSubscriptionInfo()) {
subscriptionInfoCounter.incrementAndGet();
}
if (msg.hasClaimDevice()) {
claimDeviceCounter.incrementAndGet();
}
}
public void printStats() {
int total = totalCounter.getAndSet(0);
if (total > 0) {
log.info("Transport total [{}] sessionEvents [{}] telemetry [{}] attributes [{}] getAttr [{}] subToAttr [{}] subToRpc [{}] toDevRpc [{}] " +
"toServerRpc [{}] subInfo [{}] claimDevice [{}] ",
total, sessionEventCounter.getAndSet(0), postTelemetryCounter.getAndSet(0),
postAttributesCounter.getAndSet(0), getAttributesCounter.getAndSet(0), subscribeToAttributesCounter.getAndSet(0),
subscribeToRPCCounter.getAndSet(0), toDeviceRPCCallResponseCounter.getAndSet(0),
toServerRPCCallRequestCounter.getAndSet(0), subscriptionInfoCounter.getAndSet(0), claimDeviceCounter.getAndSet(0));
}
}
}

2
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.service.update.model.UpdateMessage;
import org.thingsboard.server.common.data.UpdateMessage;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

2
application/src/main/java/org/thingsboard/server/service/update/UpdateService.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.service.update;
import org.thingsboard.server.service.update.model.UpdateMessage;
import org.thingsboard.server.common.data.UpdateMessage;
public interface UpdateService {

1
application/src/main/java/org/thingsboard/server/utils/MiscUtils.java

@ -19,6 +19,7 @@ import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.Charset;
import java.util.Random;
/**

8
application/src/main/proto/cluster.proto

@ -143,11 +143,3 @@ message DeviceStateServiceMsgProto {
bool updated = 6;
bool deleted = 7;
}
message TransactionEndServiceMsgProto {
string entityType = 1;
int64 originatorIdMSB = 2;
int64 originatorIdLSB = 3;
int64 transactionIdMSB = 4;
int64 transactionIdLSB = 5;
}

9
application/src/main/proto/jsinvoke.proto

@ -26,12 +26,9 @@ enum JsInvokeErrorCode {
}
message RemoteJsRequest {
string responseTopic = 1;
int64 requestIdMSB = 2;
int64 requestIdLSB = 3;
JsCompileRequest compileRequest = 4;
JsInvokeRequest invokeRequest = 5;
JsReleaseRequest releaseRequest = 6;
JsCompileRequest compileRequest = 1;
JsInvokeRequest invokeRequest = 2;
JsReleaseRequest releaseRequest = 3;
}
message RemoteJsResponse {

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

@ -85,6 +85,9 @@ cluster:
vitrual_nodes_size: "${CLUSTER_VIRTUAL_NODES_SIZE:16}"
# Queue partition id for current node
partition_id: "${QUEUE_PARTITION_ID:0}"
stats:
enabled: "${TB_CLUSTER_STATS_ENABLED:false}"
print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}"
# Plugins configuration parameters
plugins:
@ -183,6 +186,8 @@ cassandra:
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data types values except target to null for the same ts on save
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}"
# log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
tenant_rate_limits:
enabled: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED:false}"
configuration: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION:1000:1,30000:60}"
@ -194,6 +199,25 @@ sql:
ts_inserts_executor_type: "${SQL_TS_INSERTS_EXECUTOR_TYPE:fixed}"
# Specify thread pool size for FIXED executor service type
ts_inserts_fixed_thread_pool_size: "${SQL_TS_INSERTS_FIXED_THREAD_POOL_SIZE:200}"
# Specify batch size for persisting attribute updates
attributes:
batch_size: "${SQL_ATTRIBUTES_BATCH_SIZE:10000}"
batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}"
ts:
batch_size: "${SQL_TS_BATCH_SIZE:10000}"
batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}"
ts_latest:
batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}"
batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}"
ts_timescale:
batch_size: "${SQL_TS_TIMESCALE_BATCH_SIZE:10000}"
batch_max_delay: "${SQL_TS_TIMESCALE_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_TS_TIMESCALE_BATCH_STATS_PRINT_MS:10000}"
# Specify whether to remove null characters from strValue of attributes and timeseries before insert
remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
# Actor system parameters
actors:
@ -208,7 +232,7 @@ actors:
timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}"
rule:
# Specify thread pool size for database request callbacks executor service
db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}"
db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:50}"
# Specify thread pool size for javascript executor service
js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
# Specify thread pool size for mail sender executor service
@ -234,6 +258,7 @@ actors:
statistics:
# Enable/disable actor statistics
enabled: "${ACTORS_STATISTICS_ENABLED:true}"
js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}"
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
queue:
# Enable/disable persistence of un-processed messages to the queue
@ -278,10 +303,41 @@ redis:
# standalone or cluster
connection:
type: standalone
standalone:
host: "${REDIS_HOST:localhost}"
port: "${REDIS_PORT:6379}"
db: "${REDIS_DB:0}"
password: "${REDIS_PASSWORD:}"
useDefaultClientConfig: "${REDIS_USE_DEFAULT_CLIENT_CONFIG:true}"
# this value may be used only if you used not default ClientConfig
clientName: "${REDIS_CLIENT_NAME:standalone}"
# this value may be used only if you used not default ClientConfig
connectTimeout: "${REDIS_CLIENT_CONNECT_TIMEOUT:30000}"
# this value may be used only if you used not default ClientConfig
readTimeout: "${REDIS_CLIENT_READ_TIMEOUT:60000}"
# this value may be used only if you used not default ClientConfig
usePoolConfig: "${REDIS_CLIENT_USE_POOL_CONFIG:false}"
cluster:
# Comma-separated list of "host:port" pairs to bootstrap from.
nodes: "${REDIS_NODES:}"
# Maximum number of redirects to follow when executing commands across the cluster.
max-redirects: "${REDIS_MAX_REDIRECTS:12}"
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index
db: "${REDIS_DB:0}"
# db password
password: "${REDIS_PASSWORD:}"
# pool config
pool_config:
maxTotal: "${REDIS_POOL_CONFIG_MAX_TOTAL:128}"
maxIdle: "${REDIS_POOL_CONFIG_MAX_IDLE:128}"
minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}"
testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}"
testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}"
testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}"
minEvictableMs: "${REDIS_POOL_CONFIG_MIN_EVICTABLE_MS:60000}"
evictionRunsMs: "${REDIS_POOL_CONFIG_EVICTION_RUNS_MS:30000}"
maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}"
numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}"
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# Check new version updates parameters
updates:
@ -320,6 +376,7 @@ spring:
repositories:
enabled: "true"
jpa:
open-in-view: "false"
hibernate:
ddl-auto: "none"
database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
@ -371,6 +428,7 @@ audit-log:
state:
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:10}"
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:10}"
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
kafka:
enabled: true
@ -403,7 +461,7 @@ js:
# Specify thread pool size for JavaScript sandbox resource monitor
monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
# Maximum CPU time in milliseconds allowed for script execution
max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:100}"
max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:3000}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}"
# Remote JavaScript environment properties
@ -422,6 +480,9 @@ js:
response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}"
stats:
enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}"
print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}"
transport:
type: "${TRANSPORT_TYPE:local}" # local or remote
@ -439,6 +500,9 @@ transport:
poll_records_pack_size: "${TB_RULE_ENGINE_MAX_POLL_RECORDS:1000}"
max_poll_records_per_second: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_SECOND:10000}"
max_poll_records_per_minute: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_MINUTE:120000}"
stats:
enabled: "${TB_RULE_ENGINE_STATS_ENABLED:false}"
print_interval_ms: "${TB_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:10000}"
notifications:
topic: "${TB_TRANSPORT_NOTIFICATIONS_TOPIC:tb.transport.notifications}"
sessions:
@ -505,4 +569,4 @@ swagger:
license:
title: "${SWAGGER_LICENSE_TITLE:Apache License Version 2.0}"
url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}"
version: "${SWAGGER_VERSION:2.0}"
version: "${SWAGGER_VERSION:2.0}"

105
application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java

@ -0,0 +1,105 @@
/**
* Copyright © 2016-2019 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.cluster.routing;
import com.datastax.driver.core.utils.UUIDs;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ServerType;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class ConsistentClusterRoutingServiceTest {
private ConsistentClusterRoutingService clusterRoutingService;
private DiscoveryService discoveryService;
private String hashFunctionName = "murmur3_128";
private Integer virtualNodesSize = 1024*4;
private ServerAddress currentServer = new ServerAddress(" 100.96.1.0", 9001, ServerType.CORE);
@Before
public void setup() throws Exception {
discoveryService = mock(DiscoveryService.class);
clusterRoutingService = new ConsistentClusterRoutingService();
ReflectionTestUtils.setField(clusterRoutingService, "discoveryService", discoveryService);
ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName);
ReflectionTestUtils.setField(clusterRoutingService, "virtualNodesSize", virtualNodesSize);
when(discoveryService.getCurrentServer()).thenReturn(new ServerInstance(currentServer));
List<ServerInstance> otherServers = new ArrayList<>();
for (int i = 1; i < 30; i++) {
otherServers.add(new ServerInstance(new ServerAddress(" 100.96." + i*2 + "." + i, 9001, ServerType.CORE)));
}
when(discoveryService.getOtherServers()).thenReturn(otherServers);
clusterRoutingService.init();
}
@Test
public void testDispersionOnMillionDevices() {
List<DeviceId> devices = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
devices.add(new DeviceId(UUIDs.timeBased()));
}
testDevicesDispersion(devices);
}
private void testDevicesDispersion(List<DeviceId> devices) {
long start = System.currentTimeMillis();
Map<ServerAddress, Integer> map = new HashMap<>();
for (DeviceId deviceId : devices) {
ServerAddress address = clusterRoutingService.resolveById(deviceId).orElse(currentServer);
map.put(address, map.getOrDefault(address, 0) + 1);
}
List<Map.Entry<ServerAddress, Integer>> data = map.entrySet().stream().sorted(Comparator.comparingInt(Map.Entry::getValue)).collect(Collectors.toList());
long end = System.currentTimeMillis();
System.out.println("Size: " + virtualNodesSize + " Time: " + (end - start) + " Diff: " + (data.get(data.size() - 1).getValue() - data.get(0).getValue()));
for (Map.Entry<ServerAddress, Integer> entry : data) {
// System.out.println(entry.getKey().getHost() + ": " + entry.getValue());
}
}
}

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

3
dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java → common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java

@ -23,12 +23,13 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import java.util.List;
import java.util.concurrent.ExecutionException;
public interface ClaimDevicesService {
ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs);
ListenableFuture<ClaimResult> claimDevice(Device device, CustomerId customerId, String secretKey);
ListenableFuture<ClaimResult> claimDevice(Device device, CustomerId customerId, String secretKey) throws ExecutionException, InterruptedException;
ListenableFuture<List<Void>> reClaimDevice(TenantId tenantId, Device device);

0
dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java → common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java

0
dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java → common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java

0
dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java → common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java → common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller.claim.data;
package org.thingsboard.server.common.data;
import lombok.Data;

2
application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java → common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.update.model;
package org.thingsboard.server.common.data;
import lombok.Data;

12
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -31,6 +31,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
private CustomerId customerId;
private String name;
private String type;
private String label;
public Asset() {
super();
@ -46,6 +47,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
this.customerId = asset.getCustomerId();
this.name = asset.getName();
this.type = asset.getType();
this.label = asset.getLabel();
}
public TenantId getTenantId() {
@ -81,6 +83,14 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
this.type = type;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
@Override
public String getSearchText() {
return getName();
@ -97,6 +107,8 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
builder.append(name);
builder.append(", type=");
builder.append(type);
builder.append(", label=");
builder.append(label);
builder.append(", additionalInfo=");
builder.append(getAdditionalInfo());
builder.append(", createdTime=");

2
application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.model;
package org.thingsboard.server.common.data.security.model;
import lombok.Data;

2
application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.model;
package org.thingsboard.server.common.data.security.model;
import lombok.Data;

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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

@ -67,7 +67,7 @@ public final class TbMsg implements Serializable {
this(id, type, originator, metaData, dataType, data, new TbMsgTransactionData(id, originator), ruleChainId, ruleNodeId, clusterPartition);
}
public static ByteBuffer toBytes(TbMsg msg) {
public static byte[] toByteArray(TbMsg msg) {
MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder();
builder.setId(msg.getId().toString());
builder.setType(msg.getType());
@ -101,8 +101,16 @@ public final class TbMsg implements Serializable {
builder.setDataType(msg.getDataType().ordinal());
builder.setData(msg.getData());
byte[] bytes = builder.build().toByteArray();
return ByteBuffer.wrap(bytes);
return builder.build().toByteArray();
}
public static ByteBuffer toBytes(TbMsg msg) {
return ByteBuffer.wrap(toByteArray(msg));
}
public static TbMsg fromBytes(byte[] data) {
return fromBytes(ByteBuffer.wrap(data));
}
public static TbMsg fromBytes(ByteBuffer buffer) {
@ -115,8 +123,8 @@ public final class TbMsg implements Serializable {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
RuleChainId ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB()));
RuleNodeId ruleNodeId = null;
if(proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
}
TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData(), transactionData, ruleChainId, ruleNodeId, proto.getClusterPartition());

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

14
common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java

@ -49,9 +49,6 @@ public class TBKafkaProducerTemplate<T> {
private final KafkaProducer<String, byte[]> producer;
private final TbKafkaEncoder<T> encoder;
@Builder.Default
private TbKafkaEnricher<T> enricher = ((value, responseTopic, requestId) -> value);
private final TbKafkaPartitioner<T> partitioner;
private ConcurrentMap<String, List<PartitionInfo>> partitionInfoMap;
@Getter
@ -61,7 +58,7 @@ public class TBKafkaProducerTemplate<T> {
private final TbKafkaSettings settings;
@Builder
private TBKafkaProducerTemplate(TbKafkaSettings settings, TbKafkaEncoder<T> encoder, TbKafkaEnricher<T> enricher,
private TBKafkaProducerTemplate(TbKafkaSettings settings, TbKafkaEncoder<T> encoder,
TbKafkaPartitioner<T> partitioner, String defaultTopic, String clientId) {
Properties props = settings.toProps();
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
@ -72,7 +69,6 @@ public class TBKafkaProducerTemplate<T> {
this.settings = settings;
this.producer = new KafkaProducer<>(props);
this.encoder = encoder;
this.enricher = enricher;
this.partitioner = partitioner;
this.defaultTopic = defaultTopic;
}
@ -93,14 +89,6 @@ public class TBKafkaProducerTemplate<T> {
}
}
T enrich(T value, String responseTopic, UUID requestId) {
if (enricher != null) {
return enricher.enrich(value, responseTopic, requestId);
} else {
return value;
}
}
public Future<RecordMetadata> send(String key, T value, Callback callback) {
return send(key, value, null, callback);
}

11
common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java

@ -144,11 +144,11 @@ public class TbKafkaRequestTemplate<Request, Response> extends AbstractTbKafkaTe
tickSize = pendingRequests.size();
if (nextCleanupMs < tickTs) {
//cleanup;
pendingRequests.entrySet().forEach(kv -> {
if (kv.getValue().expTime < tickTs) {
ResponseMetaData<Response> staleRequest = pendingRequests.remove(kv.getKey());
pendingRequests.forEach((key, value) -> {
if (value.expTime < tickTs) {
ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
if (staleRequest != null) {
log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", kv.getKey(), staleRequest.expTime, tickTs);
log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs);
staleRequest.future.setException(new TimeoutException());
}
}
@ -189,13 +189,12 @@ public class TbKafkaRequestTemplate<Request, Response> extends AbstractTbKafkaTe
SettableFuture<Response> future = SettableFuture.create();
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future);
pendingRequests.putIfAbsent(requestId, responseMetaData);
request = requestTemplate.enrich(request, responseTemplate.getTopic(), requestId);
log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, key, responseMetaData.expTime);
requestTemplate.send(key, request, headers, (metadata, exception) -> {
if (exception != null) {
log.trace("[{}] Failed to post the request", requestId, exception);
} else {
log.trace("[{}] Posted the request", requestId, metadata);
log.trace("[{}] Posted the request: {}", requestId, metadata);
}
});
return future;

5
common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java

@ -32,9 +32,8 @@ import java.util.Properties;
@Component
public class TbKafkaSettings {
public static final String REQUEST_ID_HEADER = "requestId";
public static final String RESPONSE_TOPIC_HEADER = "responseTopic";
static final String REQUEST_ID_HEADER = "requestId";
static final String RESPONSE_TOPIC_HEADER = "responseTopic";
@Value("${kafka.bootstrap.servers}")
private String servers;

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

1
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -409,6 +409,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private void processDisconnect(ChannelHandlerContext ctx) {
ctx.close();
log.info("[{}] Client disconnected!", sessionId);
if (deviceSessionCtx.isConnected()) {
transportService.process(sessionInfo, AbstractTransportService.getSessionEventMsg(SessionEvent.CLOSED), null);
transportService.deregisterSession(sessionInfo);

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java

@ -47,7 +47,7 @@ public class TransportContext {
@PostConstruct
public void init() {
executor = Executors.newCachedThreadPool();
executor = Executors.newWorkStealingPool(50);
}
@PreDestroy

4
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -201,10 +201,10 @@ public class JsonConverter {
.setBoolV(value.getAsBoolean()).build());
} else if (value.isNumber()) {
result.add(buildNumericKeyValueProto(value, valueEntry.getKey()));
} else {
} else if (!value.isJsonNull()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + value);
}
} else {
} else if (!element.isJsonNull()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + element);
}
}

33
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java

@ -320,28 +320,19 @@ public class RemoteTransportService extends AbstractTransportService {
send(sessionInfo, toRuleEngineMsg, callback);
}
private static class TransportCallbackAdaptor implements Callback {
private final TransportServiceCallback<Void> callback;
TransportCallbackAdaptor(TransportServiceCallback<Void> callback) {
this.callback = callback;
}
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
if (callback != null) {
callback.onSuccess(null);
}
} else {
if (callback != null) {
callback.onError(exception);
private void send(SessionInfoProto sessionInfo, ToRuleEngineMsg toRuleEngineMsg, TransportServiceCallback<Void> callback) {
ruleEngineProducer.send(getRoutingKey(sessionInfo), toRuleEngineMsg, (metadata, exception) -> {
if (callback != null) {
if (exception == null) {
this.transportCallbackExecutor.submit(() -> {
callback.onSuccess(null);
});
} else {
this.transportCallbackExecutor.submit(() -> {
callback.onError(exception);
});
}
}
}
}
private void send(SessionInfoProto sessionInfo, ToRuleEngineMsg toRuleEngineMsg, TransportServiceCallback<Void> callback) {
ruleEngineProducer.send(getRoutingKey(sessionInfo), toRuleEngineMsg, new TransportCallbackAdaptor(callback));
});
}
}

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

4
application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java → common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.executors;
package org.thingsboard.common.util;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.thingsboard.rule.engine.api.ListeningExecutor;
import org.thingsboard.common.util.ListeningExecutor;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

6
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java → common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api;
package org.thingsboard.common.util;
import com.google.common.util.concurrent.ListenableFuture;
@ -24,4 +24,8 @@ public interface ListeningExecutor extends Executor {
<T> ListenableFuture<T> executeAsync(Callable<T> task);
default <T> ListenableFuture<T> submit(Callable<T> task) {
return executeAsync(task);
}
}

6
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>2.4.2-SNAPSHOT</version>
<version>2.4.2</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>
@ -47,6 +47,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>dao-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

37
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -52,6 +52,7 @@ import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@ -325,21 +326,21 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
private AlarmSeverity detectHighestSeverity(List<AlarmInfo> alarms) {
if (!alarms.isEmpty()) {
List<AlarmInfo> sorted = new ArrayList(alarms);
sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity()));
sorted.sort(Comparator.comparing(Alarm::getSeverity));
return sorted.get(0).getSeverity();
} else {
return null;
}
}
private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) {
log.debug("Deleting Alarm relation: {}", alarmRelation);
relationService.deleteRelationAsync(tenantId, alarmRelation).get();
relationService.deleteRelation(tenantId, alarmRelation);
}
private void createRelation(TenantId tenantId, EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
private void createRelation(TenantId tenantId, EntityRelation alarmRelation) {
log.debug("Creating Alarm relation: {}", alarmRelation);
relationService.saveRelationAsync(tenantId, alarmRelation).get();
relationService.saveRelation(tenantId, alarmRelation);
}
private Alarm merge(Alarm existing, Alarm alarm) {
@ -376,28 +377,18 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
}
private void createAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status, boolean createAnyRelation) {
try {
if (createAnyRelation) {
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM));
}
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to create relation. Status: [{}]", alarmId, status);
throw new RuntimeException(e);
if (createAnyRelation) {
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM));
}
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
}
private void deleteAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status) {
try {
deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to delete relation. Status: [{}]", alarmId, status);
throw new RuntimeException(e);
}
deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
}
private void updateAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus oldStatus, AlarmStatus newStatus) {

45
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -20,6 +20,7 @@ import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@ -28,6 +29,7 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
@ -113,7 +115,22 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
public Asset saveAsset(Asset asset) {
log.trace("Executing saveAsset [{}]", asset);
assetValidator.validate(asset, Asset::getTenantId);
return assetDao.save(asset.getTenantId(), asset);
Asset savedAsset;
if (!sqlDatabaseUsed) {
savedAsset = assetDao.save(asset.getTenantId(), asset);
} else {
try {
savedAsset = assetDao.save(asset.getTenantId(), asset);
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) {
throw new DataValidationException("Asset with such name already exists!");
} else {
throw t;
}
}
}
return savedAsset;
}
@Override
@ -265,22 +282,26 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
@Override
protected void validateCreate(TenantId tenantId, Asset asset) {
assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
d -> {
throw new DataValidationException("Asset with such name already exists!");
}
);
if (!sqlDatabaseUsed) {
assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
d -> {
throw new DataValidationException("Asset with such name already exists!");
}
);
}
}
@Override
protected void validateUpdate(TenantId tenantId, Asset asset) {
assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
d -> {
if (!d.getId().equals(asset.getId())) {
throw new DataValidationException("Asset with such name already exists!");
if (!sqlDatabaseUsed) {
assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
d -> {
if (!d.getId().equals(asset.getId())) {
throw new DataValidationException("Asset with such name already exists!");
}
}
}
);
);
}
}
@Override

73
dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java

@ -28,38 +28,58 @@ import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.thingsboard.server.common.data.id.EntityId;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false)
@EnableCaching
@Data
public class TBRedisCacheConfiguration {
public abstract class TBRedisCacheConfiguration {
@Value("${redis.connection.host}")
private String host;
@Value("${redis.pool_config.maxTotal}")
private int maxTotal;
@Value("${redis.connection.port}")
private Integer port;
@Value("${redis.pool_config.maxIdle}")
private int maxIdle;
@Value("${redis.connection.db}")
private Integer db;
@Value("${redis.pool_config.minIdle}")
private int minIdle;
@Value("${redis.connection.password}")
private String password;
@Value("${redis.pool_config.testOnBorrow}")
private boolean testOnBorrow;
@Value("${redis.pool_config.testOnReturn}")
private boolean testOnReturn;
@Value("${redis.pool_config.testWhileIdle}")
private boolean testWhileIdle;
@Value("${redis.pool_config.minEvictableMs}")
private long minEvictableMs;
@Value("${redis.pool_config.evictionRunsMs}")
private long evictionRunsMs;
@Value("${redis.pool_config.maxWaitMills}")
private long maxWaitMills;
@Value("${redis.pool_config.numberTestsPerEvictionRun}")
private int numberTestsPerEvictionRun;
@Value("${redis.pool_config.blockWhenExhausted}")
private boolean blockWhenExhausted;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setDatabase(db);
factory.setPassword(password);
return factory;
return loadFactory();
}
protected abstract JedisConnectionFactory loadFactory();
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService();
@ -74,8 +94,31 @@ public class TBRedisCacheConfiguration {
return new PreviousDeviceCredentialsIdKeyGenerator();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
return template;
}
private static void registerDefaultConverters(ConverterRegistry registry) {
Assert.notNull(registry, "ConverterRegistry must not be null!");
registry.addConverter(EntityId.class, String.class, EntityId::toString);
}
protected JedisPoolConfig buildPoolConfig() {
final JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setTestOnBorrow(testOnBorrow);
poolConfig.setTestOnReturn(testOnReturn);
poolConfig.setTestWhileIdle(testWhileIdle);
poolConfig.setMinEvictableIdleTimeMillis(minEvictableMs);
poolConfig.setTimeBetweenEvictionRunsMillis(evictionRunsMs);
poolConfig.setMaxWaitMillis(maxWaitMills);
poolConfig.setNumTestsPerEvictionRun(numberTestsPerEvictionRun);
poolConfig.setBlockWhenExhausted(blockWhenExhausted);
return poolConfig;
}
}

77
dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2019 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.cache;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Configuration
@ConditionalOnMissingBean(CaffeineCacheConfiguration.class)
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster")
public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration {
private static final String COMMA = ",";
private static final String COLON = ":";
@Value("${redis.cluster.nodes}")
private String clusterNodes;
@Value("${redis.cluster.max-redirects}")
private Integer maxRedirects;
@Value("${redis.cluster.useDefaultPoolConfig}")
private boolean useDefaultPoolConfig;
@Value("${redis.password}")
private String password;
public JedisConnectionFactory loadFactory() {
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
clusterConfiguration.setClusterNodes(getNodes(clusterNodes));
clusterConfiguration.setMaxRedirects(maxRedirects);
clusterConfiguration.setPassword(password);
if (useDefaultPoolConfig) {
return new JedisConnectionFactory(clusterConfiguration);
} else {
return new JedisConnectionFactory(clusterConfiguration, buildPoolConfig());
}
}
private List<RedisNode> getNodes(String nodes) {
List<RedisNode> result;
if (StringUtils.isBlank(nodes)) {
result = Collections.emptyList();
} else {
result = new ArrayList<>();
for (String hostPort : nodes.split(COMMA)) {
String host = hostPort.split(COLON)[0];
Integer port = Integer.valueOf(hostPort.split(COLON)[1]);
result.add(new RedisNode(host, port));
}
}
return result;
}
}

88
dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java

@ -0,0 +1,88 @@
/**
* Copyright © 2016-2019 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.cache;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import java.time.Duration;
@Configuration
@ConditionalOnMissingBean(CaffeineCacheConfiguration.class)
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone")
public class TBRedisStandaloneConfiguration extends TBRedisCacheConfiguration {
@Value("${redis.standalone.host}")
private String host;
@Value("${redis.standalone.port}")
private Integer port;
@Value("${redis.standalone.clientName}")
private String clientName;
@Value("${redis.standalone.connectTimeout}")
private Long connectTimeout;
@Value("${redis.standalone.readTimeout}")
private Long readTimeout;
@Value("${redis.standalone.useDefaultClientConfig}")
private boolean useDefaultClientConfig;
@Value("${redis.standalone.usePoolConfig}")
private boolean usePoolConfig;
@Value("${redis.db}")
private Integer db;
@Value("${redis.password}")
private String password;
public JedisConnectionFactory loadFactory() {
RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
standaloneConfiguration.setHostName(host);
standaloneConfiguration.setPort(port);
standaloneConfiguration.setDatabase(db);
standaloneConfiguration.setPassword(password);
if (useDefaultClientConfig) {
return new JedisConnectionFactory(standaloneConfiguration);
} else {
return new JedisConnectionFactory(standaloneConfiguration, buildClientConfig());
}
}
private JedisClientConfiguration buildClientConfig() {
if (usePoolConfig) {
return JedisClientConfiguration.builder()
.clientName(clientName)
.connectTimeout(Duration.ofMillis(connectTimeout))
.readTimeout(Duration.ofMillis(readTimeout))
.usePooling().poolConfig(buildPoolConfig())
.build();
} else {
return JedisClientConfiguration.builder()
.clientName(clientName)
.connectTimeout(Duration.ofMillis(connectTimeout))
.readTimeout(Duration.ofMillis(readTimeout)).build();
}
}
}

6
dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java

@ -54,11 +54,7 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic
public ComponentDescriptor saveComponent(TenantId tenantId, ComponentDescriptor component) {
componentValidator.validate(component, data -> new TenantId(EntityId.NULL_UUID));
Optional<ComponentDescriptor> result = componentDescriptorDao.saveIfNotExist(tenantId, component);
if (result.isPresent()) {
return result.get();
} else {
return componentDescriptorDao.findByClazz(tenantId, component.getClazz());
}
return result.orElseGet(() -> componentDescriptorDao.findByClazz(tenantId, component.getClazz()));
}
@Override

30
dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2019 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.device;
import lombok.Data;
import org.thingsboard.server.dao.device.claim.ClaimData;
import java.util.List;
@Data
public class ClaimDataInfo {
private final boolean fromCache;
private final List<Object> key;
private final ClaimData data;
}

55
dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
@ -37,9 +39,12 @@ import org.thingsboard.server.dao.device.claim.ClaimResponse;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import org.thingsboard.server.dao.model.ModelConstants;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE;
@ -48,6 +53,8 @@ import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CA
public class ClaimDevicesServiceImpl implements ClaimDevicesService {
private static final String CLAIM_ATTRIBUTE_NAME = "claimingAllowed";
private static final String CLAIM_DATA_ATTRIBUTE_NAME = "claimingData";
private static final ObjectMapper mapper = new ObjectMapper();
@Autowired
private DeviceService deviceService;
@ -95,24 +102,45 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
});
}
@Override
public ListenableFuture<ClaimResult> claimDevice(Device device, CustomerId customerId, String secretKey) {
private ClaimDataInfo getClaimData(Cache cache, Device device) throws ExecutionException, InterruptedException {
List<Object> key = constructCacheKey(device.getId());
ClaimData claimDataFromCache = cache.get(key, ClaimData.class);
if (claimDataFromCache != null) {
return new ClaimDataInfo(true, key, claimDataFromCache);
} else {
Optional<AttributeKvEntry> claimDataAttr = attributesService.find(device.getTenantId(), device.getId(),
DataConstants.SERVER_SCOPE, CLAIM_DATA_ATTRIBUTE_NAME).get();
if (claimDataAttr.isPresent()) {
try {
ClaimData claimDataFromAttribute = mapper.readValue(claimDataAttr.get().getValueAsString(), ClaimData.class);
return new ClaimDataInfo(false, key, claimDataFromAttribute);
} catch (IOException e) {
log.warn("Failed to read Claim Data [{}] from attribute!", claimDataAttr, e);
}
}
}
return null;
}
@Override
public ListenableFuture<ClaimResult> claimDevice(Device device, CustomerId customerId, String secretKey) throws ExecutionException, InterruptedException {
Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE);
ClaimData claimData = cache.get(key, ClaimData.class);
ClaimDataInfo claimData = getClaimData(cache, device);
if (claimData != null) {
long currTs = System.currentTimeMillis();
if (currTs > claimData.getExpirationTime() || !secretKey.equals(claimData.getSecretKey())) {
if (currTs > claimData.getData().getExpirationTime() || !secretKeyIsEmptyOrEqual(secretKey, claimData.getData().getSecretKey())) {
log.warn("The claiming timeout occurred or wrong 'secretKey' provided for the device [{}]", device.getName());
cache.evict(key);
if (claimData.isFromCache()) {
cache.evict(claimData.getKey());
}
return Futures.immediateFuture(new ClaimResult(null, ClaimResponse.FAILURE));
} else {
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
device.setCustomerId(customerId);
Device savedDevice = deviceService.saveDevice(device);
return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS));
return Futures.transform(removeClaimingSavedData(cache, claimData, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS));
}
return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED));
return Futures.transform(removeClaimingSavedData(cache, claimData, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED));
}
} else {
log.warn("Failed to find the device's claiming message![{}]", device.getName());
@ -124,6 +152,10 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
}
}
private boolean secretKeyIsEmptyOrEqual(String secretKeyA, String secretKeyB) {
return (StringUtils.isEmpty(secretKeyA) && StringUtils.isEmpty(secretKeyB)) || secretKeyA.equals(secretKeyB);
}
@Override
public ListenableFuture<List<Void>> reClaimDevice(TenantId tenantId, Device device) {
if (!device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
@ -159,13 +191,12 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
return systemDurationMs;
}
private ListenableFuture<List<Void>> removeClaimingSavedData(Cache cache, List<Object> key, Device device) {
cache.evict(key);
if (isAllowedClaimingByDefault) {
return Futures.immediateFuture(null);
private ListenableFuture<List<Void>> removeClaimingSavedData(Cache cache, ClaimDataInfo data, Device device) {
if (data.isFromCache()) {
cache.evict(data.getKey());
}
return attributesService.removeAll(device.getTenantId(),
device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME));
device.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(CLAIM_ATTRIBUTE_NAME, CLAIM_DATA_ATTRIBUTE_NAME));
}
private void cacheEviction(DeviceId deviceId) {

35
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@ -38,7 +40,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
public class DeviceCredentialsServiceImpl extends AbstractEntityService implements DeviceCredentialsService {
@Autowired
private DeviceCredentialsDao deviceCredentialsDao;
@ -78,7 +80,20 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
}
log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
credentialsValidator.validate(deviceCredentials, id -> tenantId);
return deviceCredentialsDao.save(tenantId, deviceCredentials);
if (!sqlDatabaseUsed) {
return deviceCredentialsDao.save(tenantId, deviceCredentials);
} else {
try {
return deviceCredentialsDao.save(tenantId, deviceCredentials);
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key")) {
throw new DataValidationException("Specified credentials are already registered!");
} else {
throw t;
}
}
}
}
private void formatCertData(DeviceCredentials deviceCredentials) {
@ -100,9 +115,11 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
@Override
protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) {
DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
if (existingCredentialsEntity != null) {
throw new DataValidationException("Create of existent device credentials!");
if (!sqlDatabaseUsed) {
DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
if (existingCredentialsEntity != null) {
throw new DataValidationException("Create of existent device credentials!");
}
}
}
@ -112,9 +129,11 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
if (existingCredentials == null) {
throw new DataValidationException("Unable to update non-existent device credentials!");
}
DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) {
throw new DataValidationException("Specified credentials are already registered!");
if (!sqlDatabaseUsed) {
DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) {
throw new DataValidationException("Specified credentials are already registered!");
}
}
}

43
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@ -123,7 +124,21 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
public Device saveDevice(Device device) {
log.trace("Executing saveDevice [{}]", device);
deviceValidator.validate(device, Device::getTenantId);
Device savedDevice = deviceDao.save(device.getTenantId(), device);
Device savedDevice;
if (!sqlDatabaseUsed) {
savedDevice = deviceDao.save(device.getTenantId(), device);
} else {
try {
savedDevice = deviceDao.save(device.getTenantId(), device);
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) {
throw new DataValidationException("Device with such name already exists!");
} else {
throw t;
}
}
}
if (device.getId() == null) {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId()));
@ -296,22 +311,26 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Override
protected void validateCreate(TenantId tenantId, Device device) {
deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
d -> {
throw new DataValidationException("Device with such name already exists!");
}
);
if (!sqlDatabaseUsed) {
deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
d -> {
throw new DataValidationException("Device with such name already exists!");
}
);
}
}
@Override
protected void validateUpdate(TenantId tenantId, Device device) {
deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
d -> {
if (!d.getUuidId().equals(device.getUuidId())) {
throw new DataValidationException("Device with such name already exists!");
if (!sqlDatabaseUsed) {
deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
d -> {
if (!d.getUuidId().equals(device.getUuidId())) {
throw new DataValidationException("Device with such name already exists!");
}
}
}
);
);
}
}
@Override

25
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -16,20 +16,45 @@
package org.thingsboard.server.dao.entity;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.relation.RelationService;
import javax.annotation.PostConstruct;
import java.util.Optional;
@Slf4j
public abstract class AbstractEntityService {
@Autowired
protected RelationService relationService;
@Value("${database.entities.type:sql}")
private String databaseType;
protected boolean sqlDatabaseUsed;
@PostConstruct
public void init() {
sqlDatabaseUsed = "sql".equalsIgnoreCase(databaseType);
}
protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
relationService.deleteEntityRelations(tenantId, entityId);
}
protected Optional<ConstraintViolationException> extractConstraintViolationException(Exception t) {
if (t instanceof ConstraintViolationException) {
return Optional.of ((ConstraintViolationException) t);
} else if (t.getCause() instanceof ConstraintViolationException) {
return Optional.of ((ConstraintViolationException) (t.getCause()));
} else {
return Optional.empty();
}
}
}

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -197,6 +197,7 @@ public class ModelConstants {
public static final String ASSET_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String ASSET_NAME_PROPERTY = "name";
public static final String ASSET_TYPE_PROPERTY = "type";
public static final String ASSET_LABEL_PROPERTY = "label";
public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text";

8
dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java

@ -37,6 +37,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@ -64,6 +65,9 @@ public final class AssetEntity implements SearchTextEntity<Asset> {
@Column(name = ASSET_NAME_PROPERTY)
private String name;
@Column(name = ASSET_LABEL_PROPERTY)
private String label;
@Column(name = SEARCH_TEXT_PROPERTY)
private String searchText;
@ -86,6 +90,7 @@ public final class AssetEntity implements SearchTextEntity<Asset> {
}
this.name = asset.getName();
this.type = asset.getType();
this.label = asset.getLabel();
this.additionalInfo = asset.getAdditionalInfo();
}
@ -163,8 +168,9 @@ public final class AssetEntity implements SearchTextEntity<Asset> {
}
asset.setName(name);
asset.setType(type);
asset.setLabel(label);
asset.setAdditionalInfo(additionalInfo);
return asset;
}
}
}

17
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java → dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java

@ -16,14 +16,11 @@
package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
import javax.persistence.Column;
import javax.persistence.Id;
@ -35,11 +32,10 @@ import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@MappedSuperclass
public abstract class AbsractTsKvEntity implements ToData<TsKvEntry> {
public abstract class AbstractTsKvEntity {
protected static final String SUM = "SUM";
protected static final String AVG = "AVG";
@ -50,10 +46,6 @@ public abstract class AbsractTsKvEntity implements ToData<TsKvEntry> {
@Column(name = ENTITY_ID_COLUMN)
protected String entityId;
@Id
@Column(name = TS_COLUMN)
protected Long ts;
@Id
@Column(name = KEY_COLUMN)
protected String key;
@ -70,8 +62,7 @@ public abstract class AbsractTsKvEntity implements ToData<TsKvEntry> {
@Column(name = DOUBLE_VALUE_COLUMN)
protected Double doubleValue;
@Override
public TsKvEntry toData() {
protected KvEntry getKvEntry() {
KvEntry kvEntry = null;
if (strValue != null) {
kvEntry = new StringDataEntry(key, strValue);
@ -82,14 +73,14 @@ public abstract class AbsractTsKvEntity implements ToData<TsKvEntry> {
} else if (booleanValue != null) {
kvEntry = new BooleanDataEntry(key, booleanValue);
}
return new BasicTsKvEntry(ts, kvEntry);
return kvEntry;
}
public abstract boolean isNotEmpty();
protected static boolean isAllNull(Object... args) {
for (Object arg : args) {
if(arg != null) {
if (arg != null) {
return false;
}
}

8
dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java

@ -40,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Data
@ -61,6 +62,9 @@ public final class AssetEntity extends BaseSqlEntity<Asset> implements SearchTex
@Column(name = ASSET_TYPE_PROPERTY)
private String type;
@Column(name = ASSET_LABEL_PROPERTY)
private String label;
@Column(name = SEARCH_TEXT_PROPERTY)
private String searchText;
@ -84,6 +88,7 @@ public final class AssetEntity extends BaseSqlEntity<Asset> implements SearchTex
}
this.name = asset.getName();
this.type = asset.getType();
this.label = asset.getLabel();
this.additionalInfo = asset.getAdditionalInfo();
}
@ -113,8 +118,9 @@ public final class AssetEntity extends BaseSqlEntity<Asset> implements SearchTex
}
asset.setName(name);
asset.setType(type);
asset.setLabel(label);
asset.setAdditionalInfo(additionalInfo);
return asset;
}
}
}

15
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java

@ -18,9 +18,10 @@ package org.thingsboard.server.dao.model.sqlts.timescale;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Column;
import javax.persistence.ColumnResult;
@ -35,6 +36,7 @@ import javax.persistence.SqlResultSetMappings;
import javax.persistence.Table;
import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_AVG;
import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_AVG_QUERY;
import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_COUNT;
@ -113,12 +115,16 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F
resultSetMapping = "timescaleCountMapping"
)
})
public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToData<TsKvEntry> {
public final class TimescaleTsKvEntity extends AbstractTsKvEntity implements ToData<TsKvEntry> {
@Id
@Column(name = TENANT_ID_COLUMN)
private String tenantId;
@Id
@Column(name = TS_COLUMN)
protected Long ts;
public TimescaleTsKvEntity() { }
public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType) {
@ -181,4 +187,9 @@ public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToDa
public boolean isNotEmpty() {
return ts != null && (strValue != null || longValue != null || doubleValue != null || booleanValue != null);
}
@Override
public TsKvEntry toData() {
return new BasicTsKvEntry(ts, getKvEntry());
}
}

16
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java

@ -17,9 +17,10 @@ package org.thingsboard.server.dao.model.sqlts.ts;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -30,18 +31,23 @@ import javax.persistence.IdClass;
import javax.persistence.Table;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@Entity
@Table(name = "ts_kv")
@IdClass(TsKvCompositeKey.class)
public final class TsKvEntity extends AbsractTsKvEntity implements ToData<TsKvEntry> {
public final class TsKvEntity extends AbstractTsKvEntity implements ToData<TsKvEntry> {
@Id
@Enumerated(EnumType.STRING)
@Column(name = ENTITY_TYPE_COLUMN)
private EntityType entityType;
@Id
@Column(name = TS_COLUMN)
protected Long ts;
public TsKvEntity() {
}
@ -100,9 +106,13 @@ public final class TsKvEntity extends AbsractTsKvEntity implements ToData<TsKvEn
}
}
@Override
public boolean isNotEmpty() {
return strValue != null || longValue != null || doubleValue != null || booleanValue != null;
}
@Override
public TsKvEntry toData() {
return new BasicTsKvEntry(ts, getKvEntry());
}
}

53
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvLatestEntity.java

@ -18,13 +18,9 @@ package org.thingsboard.server.dao.model.sqlts.ts;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -34,63 +30,30 @@ import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.DOUBLE_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@Entity
@Table(name = "ts_kv_latest")
@IdClass(TsKvLatestCompositeKey.class)
public final class TsKvLatestEntity implements ToData<TsKvEntry> {
public final class TsKvLatestEntity extends AbstractTsKvEntity implements ToData<TsKvEntry> {
//TODO: reafctor this and TsKvEntity to avoid code duplicates
@Id
@Enumerated(EnumType.STRING)
@Column(name = ENTITY_TYPE_COLUMN)
private EntityType entityType;
@Id
@Column(name = ENTITY_ID_COLUMN)
private String entityId;
@Id
@Column(name = KEY_COLUMN)
private String key;
@Column(name = TS_COLUMN)
private long ts;
@Column(name = BOOLEAN_VALUE_COLUMN)
private Boolean booleanValue;
@Column(name = STRING_VALUE_COLUMN)
private String strValue;
@Column(name = LONG_VALUE_COLUMN)
private Long longValue;
@Column(name = DOUBLE_VALUE_COLUMN)
private Double doubleValue;
@Override
public TsKvEntry toData() {
KvEntry kvEntry = null;
if (strValue != null) {
kvEntry = new StringDataEntry(key, strValue);
} else if (longValue != null) {
kvEntry = new LongDataEntry(key, longValue);
} else if (doubleValue != null) {
kvEntry = new DoubleDataEntry(key, doubleValue);
} else if (booleanValue != null) {
kvEntry = new BooleanDataEntry(key, booleanValue);
}
return new BasicTsKvEntry(ts, kvEntry);
return new BasicTsKvEntry(ts, getKvEntry());
}
@Override
public boolean isNotEmpty() {
return strValue != null || longValue != null || doubleValue != null || booleanValue != null;
}
}

29
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java

@ -56,19 +56,32 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor<
@Value("${cassandra.query.poll_ms:50}") long pollMs,
@Value("${cassandra.query.tenant_rate_limits.enabled}") boolean tenantRateLimitsEnabled,
@Value("${cassandra.query.tenant_rate_limits.configuration}") String tenantRateLimitsConfiguration,
@Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames) {
super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration);
@Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames,
@Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq) {
super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration, printQueriesFreq);
this.printTenantNames = printTenantNames;
}
@Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
public void printStats() {
log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] " +
"totalRateLimited [{}] totalRateLimitedTenants [{}] currBuffer [{}] ",
getQueueSize(),
totalAdded.getAndSet(0), totalLaunched.getAndSet(0), totalReleased.getAndSet(0),
totalFailed.getAndSet(0), totalExpired.getAndSet(0), totalRejected.getAndSet(0),
totalRateLimited.getAndSet(0), rateLimitedTenants.size(), concurrencyLevel.get());
int queueSize = getQueueSize();
int totalAddedValue = totalAdded.getAndSet(0);
int totalLaunchedValue = totalLaunched.getAndSet(0);
int totalReleasedValue = totalReleased.getAndSet(0);
int totalFailedValue = totalFailed.getAndSet(0);
int totalExpiredValue = totalExpired.getAndSet(0);
int totalRejectedValue = totalRejected.getAndSet(0);
int totalRateLimitedValue = totalRateLimited.getAndSet(0);
int rateLimitedTenantsValue = rateLimitedTenants.size();
int concurrencyLevelValue = concurrencyLevel.get();
if (queueSize > 0 || totalAddedValue > 0 || totalLaunchedValue > 0 || totalReleasedValue > 0 ||
totalFailedValue > 0 || totalExpiredValue > 0 || totalRejectedValue > 0 || totalRateLimitedValue > 0 || rateLimitedTenantsValue > 0
|| concurrencyLevelValue > 0) {
log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] " +
"totalRateLimited [{}] totalRateLimitedTenants [{}] currBuffer [{}] ",
queueSize, totalAddedValue, totalLaunchedValue, totalReleasedValue,
totalFailedValue, totalExpiredValue, totalRejectedValue, totalRateLimitedValue, rateLimitedTenantsValue, concurrencyLevelValue);
}
rateLimitedTenants.forEach(((tenantId, counter) -> {
if (printTenantNames) {

8
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java

@ -17,16 +17,14 @@ package org.thingsboard.server.dao.sql;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
public abstract class JpaAbstractDaoListeningExecutorService {
protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
@Autowired
protected JpaExecutorService service;
@PreDestroy
void onDestroy() {
service.shutdown();
}
}

35
dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.server.dao.util.SqlDao;
@Component
@SqlDao
public class JpaExecutorService extends AbstractListeningExecutor {
@Value("${spring.datasource.hikari.maximumPoolSize}")
private int poolSize;
@Override
protected int getThreadPollSize() {
return poolSize;
}
}

46
dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
public class ScheduledLogExecutorComponent {
private ScheduledExecutorService schedulerLogExecutor;
@PostConstruct
public void init() {
schedulerLogExecutor = Executors.newSingleThreadScheduledExecutor();
}
@PreDestroy
public void stop() {
if (schedulerLogExecutor != null) {
schedulerLogExecutor.shutdownNow();
}
}
public void scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
schedulerLogExecutor.scheduleAtFixedRate(command, initialDelay, period, unit);
}
}

116
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java

@ -0,0 +1,116 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j
public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
private final BlockingQueue<TbSqlQueueElement<E>> queue = new LinkedBlockingQueue<>();
private final AtomicInteger addedCount = new AtomicInteger();
private final AtomicInteger savedCount = new AtomicInteger();
private final AtomicInteger failedCount = new AtomicInteger();
private final TbSqlBlockingQueueParams params;
private ExecutorService executor;
private ScheduledLogExecutorComponent logExecutor;
public TbSqlBlockingQueue(TbSqlBlockingQueueParams params) {
this.params = params;
}
@Override
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction) {
this.logExecutor = logExecutor;
executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
String logName = params.getLogName();
int batchSize = params.getBatchSize();
long maxDelay = params.getMaxDelay();
List<TbSqlQueueElement<E>> entities = new ArrayList<>(batchSize);
while (!Thread.interrupted()) {
try {
long currentTs = System.currentTimeMillis();
TbSqlQueueElement<E> attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS);
if (attr == null) {
continue;
} else {
entities.add(attr);
}
queue.drainTo(entities, batchSize - 1);
boolean fullPack = entities.size() == batchSize;
log.debug("[{}] Going to save {} entities", logName, entities.size());
saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList()));
entities.forEach(v -> v.getFuture().set(null));
savedCount.addAndGet(entities.size());
if (!fullPack) {
long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs);
if (remainingDelay > 0) {
Thread.sleep(remainingDelay);
}
}
} catch (Exception e) {
failedCount.addAndGet(entities.size());
entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(e));
if (e instanceof InterruptedException) {
log.info("[{}] Queue polling was interrupted", logName);
break;
} else {
log.error("[{}] Failed to save {} entities", logName, entities.size(), e);
}
} finally {
entities.clear();
}
}
});
logExecutor.scheduleAtFixedRate(() -> {
if (queue.size() > 0 || addedCount.get() > 0 || savedCount.get() > 0 || failedCount.get() > 0) {
log.info("[{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]",
params.getLogName(), queue.size(), addedCount.getAndSet(0), savedCount.getAndSet(0), failedCount.getAndSet(0));
}
}, params.getStatsPrintIntervalMs(), params.getStatsPrintIntervalMs(), TimeUnit.MILLISECONDS);
}
@Override
public void destroy() {
if (executor != null) {
executor.shutdownNow();
}
}
@Override
public ListenableFuture<Void> add(E element) {
SettableFuture<Void> future = SettableFuture.create();
queue.add(new TbSqlQueueElement<>(future, element));
addedCount.incrementAndGet();
return future;
}
}

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

Loading…
Cancel
Save