Browse Source

Merge remote-tracking branch 'origin/feature/rule-node-debug'

pull/12177/head
Andrii Shvaika 2 years ago
parent
commit
e1ad1cb9f5
  1. 9
      application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
  2. 7
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  3. 9
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  4. 16
      application/src/main/data/upgrade/basic/schema_update.sql
  5. 71
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  6. 39
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  7. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
  8. 32
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  9. 17
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java
  10. 10
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
  11. 14
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  12. 1
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java
  14. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  16. 8
      application/src/main/resources/thingsboard.yml
  17. 949
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  18. 9
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  19. 4
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  20. 11
      application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
  21. 3
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  22. 5
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  23. 4
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  24. 9
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  25. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java
  26. 32
      common/data/src/main/java/org/thingsboard/server/common/data/HasDebugSettings.java
  27. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  28. 59
      common/data/src/main/java/org/thingsboard/server/common/data/debug/DebugSettings.java
  29. 4
      common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java
  30. 35
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  32. 3
      common/edge-api/src/main/proto/edge.proto
  33. 60
      common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java
  34. 26
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  35. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  36. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
  37. 5
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  38. 2
      dao/src/main/resources/sql/schema-entities.sql
  39. 18
      monitoring/src/main/resources/root_rule_chain.json
  40. 3
      msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json
  41. 3
      msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json
  42. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  43. 1
      ui-ngx/src/app/core/auth/auth.reducer.ts
  44. 32
      ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html
  45. 155
      ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts
  46. 66
      ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.html
  47. 134
      ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts
  48. 12
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  49. 4
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  50. 19
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  51. 1
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  52. 26
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  53. 7
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss
  54. 14
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  55. 8
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  56. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts
  57. 18
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  58. 10
      ui-ngx/src/app/shared/models/entity.models.ts
  59. 8
      ui-ngx/src/app/shared/models/rule-node.models.ts
  60. 35
      ui-ngx/src/app/shared/pipe/duration-left.pipe.ts
  61. 79
      ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts
  62. 1
      ui-ngx/src/app/shared/pipe/public-api.ts
  63. 1
      ui-ngx/src/assets/locale/locale.constant-ar_AE.json
  64. 23
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  65. 1
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  66. 1
      ui-ngx/src/assets/locale/locale.constant-lt_LT.json
  67. 1
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  68. 1
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json
  69. 1
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  70. 18
      ui-ngx/src/styles.scss

9
application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json

@ -20,7 +20,6 @@
}, },
"type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"name": "Device Profile Node", "name": "Device Profile Node",
"debugMode": false,
"configuration": { "configuration": {
"persistAlarmRulesState": false, "persistAlarmRulesState": false,
"fetchAlarmRulesStateOnStart": false "fetchAlarmRulesStateOnStart": false
@ -34,7 +33,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries", "name": "Save Timeseries",
"debugMode": false,
"configuration": { "configuration": {
"defaultTTL": 0 "defaultTTL": 0
}, },
@ -47,7 +45,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"debugMode": false,
"configurationVersion": 2, "configurationVersion": 2,
"configuration": { "configuration": {
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
@ -64,7 +61,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "Message Type Switch", "name": "Message Type Switch",
"debugMode": false,
"configuration": { "configuration": {
"version": 0 "version": 0
}, },
@ -77,7 +73,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false,
"configuration": { "configuration": {
"scriptLang": "TBEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
@ -92,7 +87,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log Other", "name": "Log Other",
"debugMode": false,
"configuration": { "configuration": {
"scriptLang": "TBEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
@ -107,7 +101,6 @@
}, },
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"name": "RPC Call Request", "name": "RPC Call Request",
"debugMode": false,
"configuration": { "configuration": {
"timeoutInSeconds": 60 "timeoutInSeconds": 60
}, },
@ -120,7 +113,6 @@
}, },
"type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode",
"name": "Push to cloud", "name": "Push to cloud",
"debugMode": false,
"configuration": { "configuration": {
"scope": "SERVER_SCOPE" "scope": "SERVER_SCOPE"
}, },
@ -133,7 +125,6 @@
}, },
"type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode",
"name": "Push to cloud", "name": "Push to cloud",
"debugMode": false,
"configuration": { "configuration": {
"scope": "SERVER_SCOPE" "scope": "SERVER_SCOPE"
}, },

7
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -19,7 +19,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries", "name": "Save Timeseries",
"debugMode": false,
"configuration": { "configuration": {
"defaultTTL": 0 "defaultTTL": 0
} }
@ -31,7 +30,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"debugMode": false,
"configurationVersion": 2, "configurationVersion": 2,
"configuration": { "configuration": {
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
@ -47,7 +45,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "Message Type Switch", "name": "Message Type Switch",
"debugMode": false,
"configuration": { "configuration": {
"version": 0 "version": 0
} }
@ -59,7 +56,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false,
"configuration": { "configuration": {
"scriptLang": "TBEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
@ -73,7 +69,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log Other", "name": "Log Other",
"debugMode": false,
"configuration": { "configuration": {
"scriptLang": "TBEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
@ -87,7 +82,6 @@
}, },
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"name": "RPC Call Request", "name": "RPC Call Request",
"debugMode": false,
"configuration": { "configuration": {
"timeoutInSeconds": 60 "timeoutInSeconds": 60
} }
@ -100,7 +94,6 @@
}, },
"type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"name": "Device Profile Node", "name": "Device Profile Node",
"debugMode": false,
"configuration": { "configuration": {
"persistAlarmRulesState": false, "persistAlarmRulesState": false,
"fetchAlarmRulesStateOnStart": false "fetchAlarmRulesStateOnStart": false

9
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -18,7 +18,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries", "name": "Save Timeseries",
"debugMode": false,
"configuration": { "configuration": {
"defaultTTL": 0 "defaultTTL": 0
} }
@ -30,7 +29,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"debugMode": false,
"configurationVersion": 2, "configurationVersion": 2,
"configuration": { "configuration": {
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
@ -46,7 +44,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "Message Type Switch", "name": "Message Type Switch",
"debugMode": false,
"configuration": { "configuration": {
"version": 0 "version": 0
} }
@ -58,7 +55,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false,
"configuration": { "configuration": {
"scriptLang": "TBEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
@ -72,7 +68,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log Other", "name": "Log Other",
"debugMode": false,
"configuration": { "configuration": {
"scriptLang": "TBEL", "scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
@ -86,7 +81,6 @@
}, },
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"name": "RPC Call Request", "name": "RPC Call Request",
"debugMode": false,
"configuration": { "configuration": {
"timeoutInSeconds": 60 "timeoutInSeconds": 60
} }
@ -99,7 +93,6 @@
}, },
"type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"name": "Device Profile Node", "name": "Device Profile Node",
"debugMode": false,
"configuration": { "configuration": {
"persistAlarmRulesState": false, "persistAlarmRulesState": false,
"fetchAlarmRulesStateOnStart": false "fetchAlarmRulesStateOnStart": false
@ -140,4 +133,4 @@
], ],
"ruleChainConnections": null "ruleChainConnections": null
} }
} }

16
application/src/main/data/upgrade/basic/schema_update.sql

@ -25,6 +25,22 @@ UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::
UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text
WHERE additional_info IS NOT NULL AND additional_info != 'null'; WHERE additional_info IS NOT NULL AND additional_info != 'null';
-- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START
ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS debug_settings varchar(1024) DEFAULT null;
DO
$$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'rule_node' AND column_name = 'debug_mode')
THEN
UPDATE rule_node SET debug_settings = '{"failuresEnabled": true, "allEnabledUntil": ' || cast((extract(epoch from now()) + 900) * 1000 as bigint) || '}' WHERE debug_mode = true; -- 15 minutes according to thingsboard.yml default settings.
ALTER TABLE rule_node DROP COLUMN debug_mode;
END IF;
END
$$;
-- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY END
-- CREATE MOBILE APP BUNDLES FROM EXISTING APPS -- CREATE MOBILE APP BUNDLES FROM EXISTING APPS

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

@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
@ -131,7 +132,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED;
* Created by ashvayka on 19.03.18. * Created by ashvayka on 19.03.18.
*/ */
@Slf4j @Slf4j
class DefaultTbContext implements TbContext { public class DefaultTbContext implements TbContext {
private final ActorSystemContext mainCtx; private final ActorSystemContext mainCtx;
private final String ruleChainName; private final String ruleChainName;
@ -145,25 +146,20 @@ class DefaultTbContext implements TbContext {
@Override @Override
public void tellSuccess(TbMsg msg) { public void tellSuccess(TbMsg msg) {
tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS), null); tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS));
} }
@Override @Override
public void tellNext(TbMsg msg, String relationType) { public void tellNext(TbMsg msg, String relationType) {
tellNext(msg, Collections.singleton(relationType), null); tellNext(msg, Collections.singleton(relationType));
} }
@Override @Override
public void tellNext(TbMsg msg, Set<String> relationTypes) { public void tellNext(TbMsg msg, Set<String> relationTypes) {
tellNext(msg, relationTypes, null); RuleNode ruleNode = nodeCtx.getSelf();
} persistDebugOutput(msg, relationTypes);
msg.getCallback().onProcessingEnd(ruleNode.getId());
private void tellNext(TbMsg msg, Set<String> relationTypes, Throwable th) { nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(), ruleNode.getId(), relationTypes, msg, null));
if (nodeCtx.getSelf().isDebugMode()) {
relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th));
}
msg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId());
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), relationTypes, msg, th != null ? th.getMessage() : null));
} }
@Override @Override
@ -189,9 +185,7 @@ class DefaultTbContext implements TbContext {
if (item == null) { if (item == null) {
ack(msg); ack(msg);
} else { } else {
if (nodeCtx.getSelf().isDebugMode()) { persistDebugOutput(msg, relationType);
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType);
}
nodeCtx.getChainActor().tell(new RuleChainOutputMsg(item.getRuleChainId(), item.getRuleNodeId(), relationType, msg)); nodeCtx.getChainActor().tell(new RuleChainOutputMsg(item.getRuleChainId(), item.getRuleNodeId(), relationType, msg));
} }
} }
@ -216,11 +210,9 @@ class DefaultTbContext implements TbContext {
} }
return; return;
} }
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain");
}
doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback( doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(
metadata -> { metadata -> {
persistDebugOutput(tbMsg, TbNodeConnectionType.TO_ROOT_RULE_CHAIN);
if (onSuccess != null) { if (onSuccess != null) {
onSuccess.run(); onSuccess.run();
} }
@ -310,8 +302,9 @@ class DefaultTbContext implements TbContext {
} }
return; return;
} }
RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId(); RuleNode ruleNode = nodeCtx.getSelf();
RuleNodeId ruleNodeId = nodeCtx.getSelf().getId(); RuleChainId ruleChainId = ruleNode.getRuleChainId();
RuleNodeId ruleNodeId = ruleNode.getId();
TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId); TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId);
TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder() TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdMSB(getTenantId().getId().getMostSignificantBits())
@ -321,12 +314,9 @@ class DefaultTbContext implements TbContext {
if (failureMessage != null) { if (failureMessage != null) {
msg.setFailureMessage(failureMessage); msg.setFailureMessage(failureMessage);
} }
if (nodeCtx.getSelf().isDebugMode()) {
relationTypes.forEach(relationType ->
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage));
}
mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback( mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(
metadata -> { metadata -> {
persistDebugOutput(tbMsg, relationTypes, null, failureMessage);
if (onSuccess != null) { if (onSuccess != null) {
onSuccess.run(); onSuccess.run();
} }
@ -342,10 +332,9 @@ class DefaultTbContext implements TbContext {
@Override @Override
public void ack(TbMsg tbMsg) { public void ack(TbMsg tbMsg) {
if (nodeCtx.getSelf().isDebugMode()) { RuleNode ruleNode = nodeCtx.getSelf();
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "ACK", null); persistDebugOutput(tbMsg, TbNodeConnectionType.ACK);
} tbMsg.getCallback().onProcessingEnd(ruleNode.getId());
tbMsg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId());
tbMsg.getCallback().onSuccess(); tbMsg.getCallback().onSuccess();
} }
@ -360,12 +349,11 @@ class DefaultTbContext implements TbContext {
@Override @Override
public void tellFailure(TbMsg msg, Throwable th) { public void tellFailure(TbMsg msg, Throwable th) {
if (nodeCtx.getSelf().isDebugMode()) { RuleNode ruleNode = nodeCtx.getSelf();
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th); persistDebugOutput(msg, Set.of(TbNodeConnectionType.FAILURE), th, null);
}
String failureMessage = getFailureMessage(th); String failureMessage = getFailureMessage(th);
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(),
nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE), ruleNode.getId(), Collections.singleton(TbNodeConnectionType.FAILURE),
msg, failureMessage)); msg, failureMessage));
} }
@ -1020,4 +1008,21 @@ class DefaultTbContext implements TbContext {
return failureMessage; return failureMessage;
} }
private void persistDebugOutput(TbMsg msg, String relationType) {
persistDebugOutput(msg, Set.of(relationType));
}
private void persistDebugOutput(TbMsg msg, Set<String> relationTypes) {
persistDebugOutput(msg, relationTypes, null, null);
}
private void persistDebugOutput(TbMsg msg, Set<String> relationTypes, Throwable error, String failureMessage) {
RuleNode ruleNode = nodeCtx.getSelf();
if (DebugModeUtil.isDebugAllAvailable(ruleNode)) {
relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, relationType, error, failureMessage));
} else if (DebugModeUtil.isDebugFailuresAvailable(ruleNode, relationTypes)) {
mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE, error, failureMessage);
}
}
} }

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.plugin.RuleNodeUpdatedMsg; import org.thingsboard.server.common.msg.plugin.RuleNodeUpdatedMsg;
@ -43,7 +44,6 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
@ -72,7 +72,6 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes; private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
private final RuleChainService service; private final RuleChainService service;
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final TbApiUsageReportClient apiUsageClient;
private String ruleChainName; private String ruleChainName;
private RuleNodeId firstId; private RuleNodeId firstId;
@ -81,7 +80,6 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext, TbActorRef parent, TbActorRef self) { RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext, TbActorRef parent, TbActorRef self) {
super(systemContext, tenantId, ruleChain.getId()); super(systemContext, tenantId, ruleChain.getId());
this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChain.getName(); this.ruleChainName = ruleChain.getName();
this.parent = parent; this.parent = parent;
this.self = self; this.self = self;
@ -138,7 +136,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
} }
Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet()); Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList()); List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).toList();
removedRules.forEach(ruleNodeId -> { removedRules.forEach(ruleNodeId -> {
log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId); log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId);
RuleNodeCtx removed = nodeActors.remove(ruleNodeId); RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
@ -177,7 +175,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
for (RuleNode ruleNode : ruleNodeList) { for (RuleNode ruleNode : ruleNodeList) {
List<EntityRelation> relations = service.getRuleNodeRelations(TenantId.SYS_TENANT_ID, ruleNode.getId()); List<EntityRelation> relations = service.getRuleNodeRelations(TenantId.SYS_TENANT_ID, ruleNode.getId());
log.trace("[{}][{}][{}] Processing rule node relations [{}]", tenantId, entityId, ruleNode.getId(), relations.size()); log.trace("[{}][{}][{}] Processing rule node relations [{}]", tenantId, entityId, ruleNode.getId(), relations.size());
if (relations.size() == 0) { if (relations.isEmpty()) {
nodeRoutes.put(ruleNode.getId(), Collections.emptyList()); nodeRoutes.put(ruleNode.getId(), Collections.emptyList());
} else { } else {
for (EntityRelation relation : relations) { for (EntityRelation relation : relations) {
@ -238,45 +236,47 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
} }
public void onRuleChainInputMsg(RuleChainInputMsg envelope) { public void onRuleChainInputMsg(RuleChainInputMsg envelope) {
var msg = envelope.getMsg(); var tbMsg = envelope.getMsg();
if (!checkMsgValid(msg)) { if (!checkMsgValid(tbMsg)) {
return; return;
} }
if (entityId.equals(envelope.getRuleChainId())) { if (entityId.equals(envelope.getRuleChainId())) {
onTellNext(envelope.getMsg(), false); onTellNext(tbMsg, false);
} else { } else {
parent.tell(envelope); parent.tell(envelope);
} }
} }
public void onRuleChainOutputMsg(RuleChainOutputMsg envelope) { public void onRuleChainOutputMsg(RuleChainOutputMsg envelope) {
var msg = envelope.getMsg(); var tbMsg = envelope.getMsg();
if (!checkMsgValid(msg)) { if (!checkMsgValid(tbMsg)) {
return; return;
} }
if (entityId.equals(envelope.getRuleChainId())) { if (entityId.equals(envelope.getRuleChainId())) {
var originatorNodeId = envelope.getTargetRuleNodeId(); var originatorNodeId = envelope.getTargetRuleNodeId();
RuleNodeCtx ruleNodeCtx = nodeActors.get(originatorNodeId); RuleNodeCtx ruleNodeCtx = nodeActors.get(originatorNodeId);
if (ruleNodeCtx != null && ruleNodeCtx.getSelf().isDebugMode()) { if (ruleNodeCtx != null) {
systemContext.persistDebugOutput(tenantId, originatorNodeId, envelope.getMsg(), envelope.getRelationType()); if (DebugModeUtil.isDebugAvailable(ruleNodeCtx.getSelf(), envelope.getRelationType())) {
systemContext.persistDebugOutput(tenantId, originatorNodeId, tbMsg, envelope.getRelationType());
}
} }
onTellNext(envelope.getMsg(), originatorNodeId, Collections.singleton(envelope.getRelationType()), RuleNodeException.UNKNOWN); onTellNext(tbMsg, originatorNodeId, Collections.singleton(envelope.getRelationType()), RuleNodeException.UNKNOWN);
} else { } else {
parent.tell(envelope); parent.tell(envelope);
} }
} }
void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) { void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) {
var msg = envelope.getMsg(); var tbMsg = envelope.getMsg();
if (!checkMsgValid(msg)) { if (!checkMsgValid(tbMsg)) {
return; return;
} }
try { try {
checkComponentStateActive(envelope.getMsg()); checkComponentStateActive(tbMsg);
if (firstNode != null) { if (firstNode != null) {
pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType()); pushMsgToNode(firstNode, tbMsg, envelope.getFromRelationType());
} else { } else {
envelope.getMsg().getCallback().onSuccess(); tbMsg.getCallback().onSuccess();
} }
} catch (RuleNodeException e) { } catch (RuleNodeException e) {
log.debug("Rule Chain is not active. Current state [{}] for processor [{}][{}] tenant [{}]", state, entityId.getEntityType(), entityId, tenantId); log.debug("Rule Chain is not active. Current state [{}] for processor [{}][{}] tenant [{}]", state, entityId.getEntityType(), entityId, tenantId);
@ -389,7 +389,8 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg, String fromRelationType) { private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg, String fromRelationType) {
if (nodeCtx != null) { if (nodeCtx != null) {
nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, ruleChainName, nodeCtx), msg, fromRelationType)); var tbCtx = new DefaultTbContext(systemContext, ruleChainName, nodeCtx);
nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(tbCtx, msg, fromRelationType));
} else { } else {
log.error("[{}][{}] RuleNodeCtx is empty", entityId, ruleChainName); log.error("[{}][{}] RuleNodeCtx is empty", entityId, ruleChainName);
msg.getCallback().onFailure(new RuleEngineException("Rule Node CTX is empty")); msg.getCallback().onFailure(new RuleEngineException("Rule Node CTX is empty"));

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

@ -37,7 +37,8 @@ public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNode
private final RuleChainId ruleChainId; private final RuleChainId ruleChainId;
private final RuleNodeId ruleNodeId; private final RuleNodeId ruleNodeId;
private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, String ruleChainName, RuleNodeId ruleNodeId) { private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId,
RuleChainId ruleChainId, String ruleChainName, RuleNodeId ruleNodeId) {
super(systemContext, tenantId, ruleNodeId); super(systemContext, tenantId, ruleNodeId);
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.ruleChainId = ruleChainId; this.ruleChainId = ruleChainId;
@ -46,7 +47,7 @@ public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNode
@Override @Override
protected RuleNodeActorMessageProcessor createProcessor(TbActorCtx ctx) { protected RuleNodeActorMessageProcessor createProcessor(TbActorCtx ctx) {
return new RuleNodeActorMessageProcessor(tenantId, this.ruleChainName, ruleNodeId, systemContext, ctx.getParentRef(), ctx); return new RuleNodeActorMessageProcessor(tenantId, this.ruleChainName, ruleNodeId, systemContext, ctx);
} }
@Override @Override
@ -118,7 +119,6 @@ public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNode
this.ruleChainId = ruleChainId; this.ruleChainId = ruleChainId;
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.ruleNodeId = ruleNodeId; this.ruleNodeId = ruleNodeId;
} }
@Override @Override

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

@ -16,11 +16,11 @@
package org.thingsboard.server.actors.ruleChain; package org.thingsboard.server.actors.ruleChain;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.TbRuleNodeUpdateException; import org.thingsboard.server.actors.TbRuleNodeUpdateException;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor; import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
@ -51,13 +51,13 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
private TbNode tbNode; private TbNode tbNode;
private RuleNodeInfo info; private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName,
, TbActorRef parent, TbActorRef self) { RuleNodeId ruleNodeId, ActorSystemContext systemContext, TbActorCtx selfActor) {
super(systemContext, tenantId, ruleNodeId); super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient(); this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode)); this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, selfActor, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, getName(ruleNode)); this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, getName(ruleNode));
} }
@ -78,7 +78,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
if (isMyNodePartition(newRuleNode)) { if (isMyNodePartition(newRuleNode)) {
this.info = new RuleNodeInfo(entityId, ruleChainName, getName(newRuleNode)); this.info = new RuleNodeInfo(entityId, ruleChainName, getName(newRuleNode));
boolean restartRequired = state != ComponentLifecycleState.ACTIVE || boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); !(ruleNode.getType().equals(newRuleNode.getType()) &&
ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.ruleNode = newRuleNode; this.ruleNode = newRuleNode;
this.defaultCtx.updateSelf(newRuleNode); this.defaultCtx.updateSelf(newRuleNode);
if (restartRequired) { if (restartRequired) {
@ -125,12 +126,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
checkComponentStateActive(msg.getMsg()); checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg(); TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter(); int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage(); var tenantProfileConfiguration = getTenantProfileConfiguration();
int maxRuleNodeExecutionsPerMessage = tenantProfileConfiguration.getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) { if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT); apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) { persistDebugInputIfAllowed(msg.getMsg(), "Self");
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
}
try { try {
tbNode.onMsg(defaultCtx, msg.getMsg()); tbNode.onMsg(defaultCtx, msg.getMsg());
} catch (Exception e) { } catch (Exception e) {
@ -149,12 +149,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
checkComponentStateActive(msg.getMsg()); checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg(); TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter(); int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage(); var tenantProfileConfiguration = getTenantProfileConfiguration();
int maxRuleNodeExecutionsPerMessage = tenantProfileConfiguration.getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) { if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT); apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) { persistDebugInputIfAllowed(msg.getMsg(), msg.getFromRelationType());
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try { try {
tbNode.onMsg(msg.getCtx(), msg.getMsg()); tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) { } catch (Exception e) {
@ -216,4 +215,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null); systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
defaultCtx.ack(source); defaultCtx.ack(source);
} }
private void persistDebugInputIfAllowed(TbMsg msg, String fromNodeConnectionType) {
if (DebugModeUtil.isDebugAllAvailable(ruleNode)) {
systemContext.persistDebugInput(tenantId, entityId, msg, fromNodeConnectionType);
}
}
} }

17
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java

@ -15,8 +15,8 @@
*/ */
package org.thingsboard.server.actors.ruleChain; package org.thingsboard.server.actors.ruleChain;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
@ -25,10 +25,21 @@ import org.thingsboard.server.common.data.rule.RuleNode;
* Created by ashvayka on 19.03.18. * Created by ashvayka on 19.03.18.
*/ */
@Data @Data
@AllArgsConstructor public final class RuleNodeCtx {
final class RuleNodeCtx {
private final TenantId tenantId; private final TenantId tenantId;
private final TbActorRef chainActor; private final TbActorRef chainActor;
private final TbActorRef selfActor; private final TbActorRef selfActor;
private RuleNode self; private RuleNode self;
RuleNodeCtx(TenantId tenantId, TbActorCtx selfActor, RuleNode self) {
this(tenantId, selfActor.getParentRef(), selfActor, self);
}
RuleNodeCtx(TenantId tenantId, TbActorRef chainActor, TbActorRef selfActor, RuleNode self) {
this.tenantId = tenantId;
this.chainActor = chainActor;
this.selfActor = selfActor;
this.self = self;
}
} }

10
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java

@ -26,24 +26,24 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbRuleEngineActorMsg; import org.thingsboard.server.common.msg.TbRuleEngineActorMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Set; import java.util.Set;
/** /**
* Created by ashvayka on 19.03.18. * Created by ashvayka on 19.03.18.
*/ */
@Getter
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString @ToString
class RuleNodeToRuleChainTellNextMsg extends TbRuleEngineActorMsg implements Serializable { public class RuleNodeToRuleChainTellNextMsg extends TbRuleEngineActorMsg implements Serializable {
@Serial
private static final long serialVersionUID = 4577026446412871820L; private static final long serialVersionUID = 4577026446412871820L;
@Getter
private final RuleChainId ruleChainId; private final RuleChainId ruleChainId;
@Getter
private final RuleNodeId originator; private final RuleNodeId originator;
@Getter
private final Set<String> relationTypes; private final Set<String> relationTypes;
@Getter
private final String failureMessage; private final String failureMessage;
public RuleNodeToRuleChainTellNextMsg(RuleChainId ruleChainId, RuleNodeId originator, Set<String> relationTypes, TbMsg tbMsg, String failureMessage) { public RuleNodeToRuleChainTellNextMsg(RuleChainId ruleChainId, RuleNodeId originator, Set<String> relationTypes, TbMsg tbMsg, String failureMessage) {

14
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -28,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.SystemParams;
@ -70,6 +71,15 @@ public class SystemInfoController extends BaseController {
@Value("${ui.dashboard.max_datapoints_limit}") @Value("${ui.dashboard.max_datapoints_limit}")
private long maxDatapointsLimit; private long maxDatapointsLimit;
@Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes;
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled:true}")
private boolean ruleChainDebugPerTenantLimitsEnabled;
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.configuration:50000:3600}")
private String ruleChainDebugPerTenantLimitsConfiguration;
@Autowired(required = false) @Autowired(required = false)
private BuildProperties buildProperties; private BuildProperties buildProperties;
@ -141,6 +151,10 @@ public class SystemInfoController extends BaseController {
if (!currentUser.isSystemAdmin()) { if (!currentUser.isSystemAdmin()) {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
systemParams.setMaxDebugModeDurationMinutes(DebugModeUtil.getMaxDebugAllDuration(tenantProfileConfiguration.getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes));
if (ruleChainDebugPerTenantLimitsEnabled) {
systemParams.setRuleChainDebugPerTenantLimitsConfiguration(ruleChainDebugPerTenantLimitsConfiguration);
}
} }
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)

1
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -151,6 +151,7 @@ public class TenantProfileController extends BaseController {
" \"maxJSExecutions\": 5000000,\n" + " \"maxJSExecutions\": 5000000,\n" +
" \"maxDPStorageDays\": 0,\n" + " \"maxDPStorageDays\": 0,\n" +
" \"maxRuleNodeExecutionsPerMessage\": 50,\n" + " \"maxRuleNodeExecutionsPerMessage\": 50,\n" +
" \"maxDebugModeDurationMinutes\": 15,\n" +
" \"maxEmails\": 0,\n" + " \"maxEmails\": 0,\n" +
" \"maxSms\": 0,\n" + " \"maxSms\": 0,\n" +
" \"maxCreatedAlarms\": 1000,\n" + " \"maxCreatedAlarms\": 1000,\n" +

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java

@ -88,7 +88,7 @@ public abstract class BaseRuleChainMetadataConstructor implements RuleChainMetad
.setIdLSB(node.getId().getId().getLeastSignificantBits()) .setIdLSB(node.getId().getId().getLeastSignificantBits())
.setType(node.getType()) .setType(node.getType())
.setName(node.getName()) .setName(node.getName())
.setDebugMode(node.isDebugMode()) .setDebugSettings(JacksonUtil.toString(node.getDebugSettings()))
.setConfiguration(JacksonUtil.toString(node.getConfiguration())) .setConfiguration(JacksonUtil.toString(node.getConfiguration()))
.setAdditionalInfo(JacksonUtil.toString(node.getAdditionalInfo())) .setAdditionalInfo(JacksonUtil.toString(node.getAdditionalInfo()))
.setSingletonMode(node.isSingletonMode()) .setSingletonMode(node.isSingletonMode())

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java

@ -88,6 +88,7 @@ public class TenantMsgConstructorV1 implements TenantMsgConstructor {
configuration.setMaxTransportDataPoints(0); configuration.setMaxTransportDataPoints(0);
configuration.setRuleEngineExceptionsTtlDays(0); configuration.setRuleEngineExceptionsTtlDays(0);
configuration.setMaxRuleNodeExecutionsPerMessage(0); configuration.setMaxRuleNodeExecutionsPerMessage(0);
configuration.setMaxDebugModeDurationMinutes(0);
tenantProfileData.setConfiguration(configuration); tenantProfileData.setConfiguration(configuration);
tenantProfile.setProfileData(tenantProfileData); tenantProfile.setProfileData(tenantProfileData);

2
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -202,7 +202,7 @@ public class InstallScripts {
ruleChain = ruleChainService.saveRuleChain(ruleChain, false); ruleChain = ruleChainService.saveRuleChain(ruleChain, false);
ruleChainMetaData.setRuleChainId(ruleChain.getId()); ruleChainMetaData.setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData, Function.identity(), false); ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity(), false);
return ruleChain; return ruleChain;
} }

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

@ -465,7 +465,7 @@ actors:
debug_mode_rate_limits_per_tenant: debug_mode_rate_limits_per_tenant:
# Enable/Disable the rate limit of persisted debug events for all rule nodes per tenant # Enable/Disable the rate limit of persisted debug events for all rule nodes per tenant
enabled: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}" enabled: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
# The value of DEBUG mode rate limit. By default, no more then 50 thousand events per hour # The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour
configuration: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" configuration: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}"
node: node:
# Errors for particular actor are persisted once per specified amount of milliseconds # Errors for particular actor are persisted once per specified amount of milliseconds
@ -505,6 +505,12 @@ actors:
# Actors statistic persistence frequency in milliseconds # Actors statistic persistence frequency in milliseconds
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}" persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
debug:
settings:
# Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one.
# If value from this setting is invalid, the default value (15 minutes) will be used.
default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}"
# Cache settings parameters # Cache settings parameters
cache: cache:
# caffeine or redis(7.2 - latest compatible version) # caffeine or redis(7.2 - latest compatible version)

949
application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java

@ -0,0 +1,949 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.rule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.ruleChain.DefaultTbContext;
import org.thingsboard.server.actors.ruleChain.RuleChainOutputMsg;
import org.thingsboard.server.actors.ruleChain.RuleNodeCtx;
import org.thingsboard.server.actors.ruleChain.RuleNodeToRuleChainTellNextMsg;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingStackItem;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.common.SimpleTbQueueCallback;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@SuppressWarnings("ResultOfMethodCallIgnored")
@ExtendWith(MockitoExtension.class)
class DefaultTbContextTest {
private final String EXCEPTION_MSG = "Some runtime exception!";
private final RuntimeException EXCEPTION = new RuntimeException(EXCEPTION_MSG);
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("c7bf4c85-923c-4688-a4b5-0f8a0feb7cd5"));
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("1ca5e2ef-1309-41d9-bafa-709e9df0e2a6"));
private final RuleChainId RULE_CHAIN_ID = new RuleChainId(UUID.fromString("b87c4123-f9f2-41a6-9a09-e3a5b6580b11"));
@Mock
private ActorSystemContext mainCtxMock;
@Mock
private RuleNodeCtx nodeCtxMock;
@Mock
private TbActorRef chainActorMock;
private DefaultTbContext defaultTbContext;
@BeforeEach
public void setUp() {
defaultTbContext = new DefaultTbContext(mainCtxMock, "Test rule chain name", nodeCtxMock);
}
@Test
public void givenDebugFailuresEvents_whenTellSuccess_thenVerifyDebugOutputNotPersisted() {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellSuccess(msg);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).shouldHaveNoInteractions();
checkTellNextCommonLogic(callbackMock, TbNodeConnectionType.SUCCESS, msg);
}
@Test
public void givenDebugFailuresEventsAndSuccessConnection_whenTellNext_thenVerifyDebugOutputNotPersisted() {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellNext(msg, TbNodeConnectionType.SUCCESS);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).shouldHaveNoInteractions();
checkTellNextCommonLogic(callbackMock, TbNodeConnectionType.SUCCESS, msg);
}
@MethodSource
@ParameterizedTest
void givenDebugFailuresEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted(Set<String> connections) {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellNext(msg, connections);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, null, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
checkTellNextCommonLogic(callbackMock, connections, msg);
}
private static Stream<Set<String>> givenDebugFailuresEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted() {
return Stream.of(
Collections.singleton(TbNodeConnectionType.FAILURE),
Set.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS)
);
}
@MethodSource
@ParameterizedTest
void givenDebugDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted(Set<String> connections) {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.off());
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellNext(msg, connections);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).shouldHaveNoInteractions();
checkTellNextCommonLogic(callbackMock, connections, msg);
}
private static Stream<Set<String>> givenDebugDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted() {
return Stream.of(
Collections.singleton(TbNodeConnectionType.FAILURE),
Collections.singleton(TbNodeConnectionType.SUCCESS),
Set.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS)
);
}
@MethodSource
@ParameterizedTest
void givenDebugAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellNext(msg, connection);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
checkTellNextCommonLogic(callbackMock, connection, msg);
}
private static Stream<String> givenDebugAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() {
return failureAndSuccessConnection();
}
@Test
public void givenDebugAllEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
Set<String> connections = failureAndSuccessConnection().collect(Collectors.toSet());
defaultTbContext.tellNext(msg, connections);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class);
int wantedNumberOfInvocations = connections.size();
then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture(), nullable(Throwable.class), nullable(String.class));
then(mainCtxMock).shouldHaveNoMoreInteractions();
assertThat(nodeConnectionsCaptor.getAllValues()).hasSize(wantedNumberOfInvocations);
assertThat(nodeConnectionsCaptor.getAllValues()).containsExactlyInAnyOrderElementsOf(connections);
checkTellNextCommonLogic(callbackMock, connections, msg);
}
@MethodSource
@ParameterizedTest
void givenDebugAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellNext(msg, connection);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
checkTellNextCommonLogic(callbackMock, connection, msg);
}
private static Stream<String> givenDebugAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() {
return failureAndSuccessConnection();
}
@Test
public void givenDebugAllThenOnlyEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failuresOrUntil(getUntilTime()));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
Set<String> connections = failureAndSuccessConnection().collect(Collectors.toSet());
defaultTbContext.tellNext(msg, connections);
// THEN
then(nodeCtxMock).should().getChainActor();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class);
int wantedNumberOfInvocations = connections.size();
then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture(), nullable(Throwable.class), nullable(String.class));
then(mainCtxMock).shouldHaveNoMoreInteractions();
assertThat(nodeConnectionsCaptor.getAllValues()).hasSize(wantedNumberOfInvocations);
assertThat(nodeConnectionsCaptor.getAllValues()).containsExactlyInAnyOrderElementsOf(connections);
checkTellNextCommonLogic(callbackMock, connections, msg);
}
private static Stream<String> failureAndSuccessConnection() {
return Stream.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS);
}
@Test
public void givenDebugFailuresEventsAndFailureConnection_whenOutput_thenVerifyDebugOutputPersisted() {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.output(msgMock, TbNodeConnectionType.FAILURE);
// THEN
checkOutputCommonLogic(msgMock, TbNodeConnectionType.FAILURE);
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.FAILURE, null, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenDebugFailuresEventsAndSuccessConnection_whenOutput_thenVerifyDebugOutputNotPersisted() {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID));
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS);
// THEN
checkOutputCommonLogic(msgMock, TbNodeConnectionType.SUCCESS);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@ParameterizedTest
@ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE})
void givenDebugDisabled_whenOutput_thenVerifyDebugOutputNotPersisted(String nodeConnection) {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID));
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.output(msgMock, nodeConnection);
// THEN
checkOutputCommonLogic(msgMock, nodeConnection);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@ParameterizedTest
@ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE})
void givenDebugAllEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.output(msgMock, nodeConnection);
// THEN
checkOutputCommonLogic(msgMock, nodeConnection);
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection, null, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@ParameterizedTest
@ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE})
void givenDebugAllThenOnlyFailureEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.output(msgMock, nodeConnection);
// THEN
checkOutputCommonLogic(msgMock, nodeConnection);
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection, null, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenEmptyStack_whenOutput_thenVerifyMsgAck() {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
given(msgMock.popFormStack()).willReturn(null);
TbMsgCallback callbackMock = mock(TbMsgCallback.class);
given(msgMock.getCallback()).willReturn(callbackMock);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
// WHEN
defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS);
// THEN
then(msgMock).should().popFormStack();
then(callbackMock).should().onProcessingEnd(RULE_NODE_ID);
then(callbackMock).should().onSuccess();
then(nodeCtxMock).should(never()).getChainActor();
}
@Test
public void givenEmptyStackAndDebugAllEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(msgMock.popFormStack()).willReturn(null);
TbMsgCallback callbackMock = mock(TbMsgCallback.class);
given(msgMock.getCallback()).willReturn(callbackMock);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
// WHEN
defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS);
// THEN
then(msgMock).should().popFormStack();
then(callbackMock).should().onProcessingEnd(RULE_NODE_ID);
then(callbackMock).should().onSuccess();
then(nodeCtxMock).should(never()).getChainActor();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK, null, null);
}
@Test
public void givenEmptyStackAndDebugAllThenOnlyFailureEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() {
// GIVEN
var msgMock = mock(TbMsg.class);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failuresOrUntil(getUntilTime()));
given(msgMock.popFormStack()).willReturn(null);
TbMsgCallback callbackMock = mock(TbMsgCallback.class);
given(msgMock.getCallback()).willReturn(callbackMock);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
// WHEN
defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS);
// THEN
then(msgMock).should().popFormStack();
then(callbackMock).should().onProcessingEnd(RULE_NODE_ID);
then(callbackMock).should().onSuccess();
then(nodeCtxMock).should(never()).getChainActor();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK, null, null);
}
@Test
public void givenDebugFailuresEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() {
// GIVEN
var msg = getTbMsgWithQueueName();
var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
var tbClusterServiceMock = mock(TbClusterService.class);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi);
given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock);
// WHEN
defaultTbContext.enqueueForTellFailure(msg, EXCEPTION);
// THEN
then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID);
TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID);
checkEnqueueForTellFailurePushMsgToRuleEngine(tbClusterServiceMock, tpi, expectedTbMsg);
ArgumentCaptor<TbMsg> tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(TbNodeConnectionType.FAILURE), isNull(), eq(EXCEPTION_MSG));
TbMsg actualTbMsg = tbMsgCaptor.getValue();
assertThat(actualTbMsg).usingRecursiveComparison()
.ignoringFields("id", "ctx")
.isEqualTo(expectedTbMsg);
then(mainCtxMock).should().getClusterService();
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(tbClusterServiceMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenDebugDisabled_whenEnqueueForTellFailure_thenVerifyDebugOutputNotPersisted() {
// GIVEN
var msg = getTbMsgWithQueueName();
var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
var tbClusterServiceMock = mock(TbClusterService.class);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi);
given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock);
// WHEN
defaultTbContext.enqueueForTellFailure(msg, EXCEPTION);
// THEN
then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID);
TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID);
checkEnqueueForTellFailurePushMsgToRuleEngine(tbClusterServiceMock, tpi, expectedTbMsg);
then(mainCtxMock).should().getClusterService();
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(tbClusterServiceMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenDebugAllEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() {
// GIVEN
var msg = getTbMsgWithQueueName();
var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
var tbClusterServiceMock = mock(TbClusterService.class);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi);
given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock);
// WHEN
defaultTbContext.enqueueForTellFailure(msg, EXCEPTION);
// THEN
then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID);
TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID);
checkEnqueueForTellFailurePushMsgToRuleEngine(tbClusterServiceMock, tpi, expectedTbMsg);
ArgumentCaptor<TbMsg> tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(TbNodeConnectionType.FAILURE), isNull(), eq(EXCEPTION_MSG));
TbMsg actualTbMsg = tbMsgCaptor.getValue();
assertThat(actualTbMsg).usingRecursiveComparison()
.ignoringFields("id", "ctx")
.isEqualTo(expectedTbMsg);
then(mainCtxMock).should().getClusterService();
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(tbClusterServiceMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenInvalidMsg_whenEnqueueForTellFailure_thenDoNothing() {
// GIVEN
var msgMock = mock(TbMsg.class);
var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true);
given(msgMock.getOriginator()).willReturn(TENANT_ID);
given(msgMock.getQueueName()).willReturn(DataConstants.MAIN_QUEUE_NAME);
given(msgMock.isValid()).willReturn(false);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi);
// WHEN
defaultTbContext.enqueueForTellFailure(msgMock, EXCEPTION);
// THEN
then(msgMock).should(times(2)).getQueueName();
then(msgMock).should().getOriginator();
then(msgMock).should().isValid();
then(msgMock).shouldHaveNoMoreInteractions();
then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).should(times(2)).getTenantId();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
then(chainActorMock).shouldHaveNoInteractions();
}
@MethodSource
@ParameterizedTest
void givenDebugOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForDebugAll(boolean debugFailures, long debugAllUntil, String connectionType) {
// GIVEN
var msg = getTbMsgWithQueueName();
var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(new DebugSettings(debugFailures, debugAllUntil));
var tbClusterServiceMock = mock(TbClusterService.class);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi);
given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock);
// WHEN
defaultTbContext.enqueueForTellNext(msg, connectionType);
// THEN
then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID);
TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID);
ArgumentCaptor<ToRuleEngineMsg> toRuleEngineMsgCaptor = ArgumentCaptor.forClass(ToRuleEngineMsg.class);
ArgumentCaptor<SimpleTbQueueCallback> simpleTbQueueCallbackCaptor = ArgumentCaptor.forClass(SimpleTbQueueCallback.class);
then(tbClusterServiceMock).should().pushMsgToRuleEngine(eq(tpi), notNull(UUID.class), toRuleEngineMsgCaptor.capture(), simpleTbQueueCallbackCaptor.capture());
ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue();
assertThat(actualToRuleEngineMsg).usingRecursiveComparison()
.ignoringFields("tbMsg_")
.isEqualTo(ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(expectedTbMsg))
.addAllRelationTypes(List.of(connectionType)).build());
var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue();
assertThat(simpleTbQueueCallback).isNotNull();
simpleTbQueueCallback.onSuccess(null);
if (debugAllUntil > 0) {
ArgumentCaptor<TbMsg> tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(connectionType), isNull(), isNull());
TbMsg actualTbMsg = tbMsgCaptor.getValue();
assertThat(actualTbMsg).usingRecursiveComparison()
.ignoringFields("id", "ctx")
.isEqualTo(expectedTbMsg);
}
then(mainCtxMock).should().getClusterService();
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(tbClusterServiceMock).shouldHaveNoMoreInteractions();
}
@MethodSource
@ParameterizedTest
void givenDebugOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForDebugAll(boolean debugFailures, long debugAllUntil) {
// GIVEN
var msg = getTbMsgWithQueueName();
var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setQueueName(DataConstants.MAIN_QUEUE_NAME);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(new DebugSettings(debugFailures, debugAllUntil));
var tbClusterServiceMock = mock(TbClusterService.class);
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi);
given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock);
Consumer<Throwable> onFailure = mock(Consumer.class);
Runnable onSuccess = mock(Runnable.class);
// WHEN
defaultTbContext.enqueue(msg, onSuccess, onFailure);
// THEN
then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID);
TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID);
ArgumentCaptor<ToRuleEngineMsg> toRuleEngineMsgCaptor = ArgumentCaptor.forClass(ToRuleEngineMsg.class);
ArgumentCaptor<SimpleTbQueueCallback> simpleTbQueueCallbackCaptor = ArgumentCaptor.forClass(SimpleTbQueueCallback.class);
then(tbClusterServiceMock).should().pushMsgToRuleEngine(eq(tpi), notNull(UUID.class), toRuleEngineMsgCaptor.capture(), simpleTbQueueCallbackCaptor.capture());
ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue();
assertThat(actualToRuleEngineMsg).usingRecursiveComparison()
.ignoringFields("tbMsg_")
.isEqualTo(ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(expectedTbMsg))
.build());
var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue();
assertThat(simpleTbQueueCallback).isNotNull();
simpleTbQueueCallback.onSuccess(null);
if (debugAllUntil > 0) {
then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), eq(TbNodeConnectionType.TO_ROOT_RULE_CHAIN), nullable(Throwable.class), nullable(String.class));
}
then(mainCtxMock).should().getClusterService();
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(tbClusterServiceMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenDebugFailuress_whenTellFailure_thenVerifyDebugOutputPersisted() {
// GIVEN
var msg = getTbMsg();
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellFailure(msg, EXCEPTION);
// THEN
var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg(
RULE_CHAIN_ID,
RULE_NODE_ID,
Collections.singleton(TbNodeConnectionType.FAILURE),
msg,
EXCEPTION_MSG
);
then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg);
then(chainActorMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).should().getChainActor();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenDebugDisabled_whenTellFailure_thenVerifyDebugOutputNotPersisted() {
// GIVEN
var msg = getTbMsg();
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellFailure(msg, EXCEPTION);
// THEN
var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg(
RULE_CHAIN_ID,
RULE_NODE_ID,
Collections.singleton(TbNodeConnectionType.FAILURE),
msg,
EXCEPTION_MSG
);
then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg);
then(chainActorMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).should().getChainActor();
then(mainCtxMock).shouldHaveNoInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@Test
public void givenDebugAllEvents_whenTellFailure_thenVerifyDebugOutputPersisted() {
// GIVEN
var msg = getTbMsg();
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.until(getUntilTime()));
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellFailure(msg, EXCEPTION);
// THEN
var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg(
RULE_CHAIN_ID,
RULE_NODE_ID,
Collections.singleton(TbNodeConnectionType.FAILURE),
msg,
EXCEPTION_MSG
);
then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg);
then(chainActorMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).should().getChainActor();
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION, null);
then(mainCtxMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).shouldHaveNoMoreInteractions();
}
@MethodSource
@ParameterizedTest
void givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(boolean debugFailures,
long debugAllUntil,
String connection,
boolean shouldPersist,
boolean shouldPersistAfterDurationTime) {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);
var ruleNode = new RuleNode(RULE_NODE_ID);
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(new DebugSettings(debugFailures, debugAllUntil));
if (shouldPersist) {
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
}
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
given(nodeCtxMock.getChainActor()).willReturn(chainActorMock);
// WHEN
defaultTbContext.tellNext(msg, connection);
// THEN
if (shouldPersist) {
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null);
}
// GIVEN
Mockito.clearInvocations(mainCtxMock);
ruleNode.setDebugSettings(new DebugSettings(ruleNode.getDebugSettings().isFailuresEnabled(), 0));
// WHEN
defaultTbContext.tellNext(msg, connection);
// THEN
if (shouldPersistAfterDurationTime) {
then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null);
}
}
private void checkTellNextCommonLogic(TbMsgCallback callbackMock, String nodeConnection, TbMsg msg) {
checkTellNextCommonLogic(callbackMock, Collections.singleton(nodeConnection), msg);
}
private void checkTellNextCommonLogic(TbMsgCallback callbackMock, Set<String> nodeConnections, TbMsg msg) {
then(callbackMock).should().onProcessingEnd(RULE_NODE_ID);
then(callbackMock).shouldHaveNoMoreInteractions();
var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg(
RULE_CHAIN_ID,
RULE_NODE_ID,
nodeConnections,
msg,
null);
then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg);
then(chainActorMock).shouldHaveNoMoreInteractions();
}
private void checkOutputCommonLogic(TbMsg msg, String nodeConnection) {
then(msg).should().popFormStack();
var expectedRuleChainOutputMsg = new RuleChainOutputMsg(
RULE_CHAIN_ID,
RULE_NODE_ID,
nodeConnection,
msg);
then(chainActorMock).should().tell(expectedRuleChainOutputMsg);
then(chainActorMock).shouldHaveNoMoreInteractions();
then(nodeCtxMock).should().getChainActor();
}
private void checkEnqueueForTellFailurePushMsgToRuleEngine(TbClusterService tbClusterService, TopicPartitionInfo tpi, TbMsg expectedTbMsg) {
ArgumentCaptor<ToRuleEngineMsg> toRuleEngineMsgCaptor = ArgumentCaptor.forClass(ToRuleEngineMsg.class);
ArgumentCaptor<SimpleTbQueueCallback> simpleTbQueueCallbackCaptor = ArgumentCaptor.forClass(SimpleTbQueueCallback.class);
then(tbClusterService).should().pushMsgToRuleEngine(eq(tpi), notNull(UUID.class), toRuleEngineMsgCaptor.capture(), simpleTbQueueCallbackCaptor.capture());
ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue();
assertThat(actualToRuleEngineMsg).usingRecursiveComparison()
.ignoringFields("tbMsg_")
.isEqualTo(ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(expectedTbMsg))
.setFailureMessage(EXCEPTION_MSG)
.addAllRelationTypes(List.of(TbNodeConnectionType.FAILURE)).build());
var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue();
assertThat(simpleTbQueueCallback).isNotNull();
simpleTbQueueCallback.onSuccess(null);
}
private static Stream<Arguments> givenDebugOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForDebugAll() {
return Stream.of(
Arguments.of(false, getUntilTime(), TbNodeConnectionType.OTHER),
Arguments.of(true, getUntilTime(), TbNodeConnectionType.OTHER),
Arguments.of(true, 0, TbNodeConnectionType.TRUE),
Arguments.of(false, 0, TbNodeConnectionType.FALSE)
);
}
private static Stream<Arguments> givenDebugOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForDebugAll() {
return Stream.of(
Arguments.of(false, getUntilTime()),
Arguments.of(true, getUntilTime()),
Arguments.of(true, 0),
Arguments.of(false, 0)
);
}
private static Stream<Arguments> givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence() {
return Stream.of(
Arguments.of(false, getUntilTime(), TbNodeConnectionType.SUCCESS, true, false),
Arguments.of(false, getUntilTime(), TbNodeConnectionType.FAILURE, true, false),
Arguments.of(true, getUntilTime(), TbNodeConnectionType.SUCCESS, true, false),
Arguments.of(true, getUntilTime(), TbNodeConnectionType.FAILURE, true, true),
Arguments.of(true, 0, TbNodeConnectionType.SUCCESS, false, false),
Arguments.of(true, 0, TbNodeConnectionType.FAILURE, true, true),
Arguments.of(false, 0, TbNodeConnectionType.SUCCESS, false, false),
Arguments.of(false, 0, TbNodeConnectionType.FAILURE, false, false)
);
}
private TbMsg getTbMsgWithCallback(TbMsgCallback callback) {
return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING, callback);
}
private TbMsg getTbMsgWithQueueName() {
return TbMsg.newMsg(DataConstants.MAIN_QUEUE_NAME, TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
}
private TbMsg getTbMsg() {
return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
}
private static long getUntilTime() {
return getUntilTime(15);
}
private static long getUntilTime(int maxRuleNodeDebugModeDurationMinutes) {
return System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(maxRuleNodeDebugModeDurationMinutes);
}
}

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

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
@ -103,7 +104,6 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Random;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -125,8 +125,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected EdgeImitator edgeImitator; protected EdgeImitator edgeImitator;
protected Edge edge; protected Edge edge;
private final Random random = new Random();
@Autowired @Autowired
protected EdgeEventService edgeEventService; protected EdgeEventService edgeEventService;
@ -166,7 +164,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
} }
private RuleChainId getEdgeRootRuleChainId() throws Exception { private RuleChainId getEdgeRootRuleChainId() throws Exception {
return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference<PageData<RuleChain>>() {}, return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference<PageData<RuleChain>>() {
},
new PageLink(100, 0, "Edge Root Rule Chain"), new PageLink(100, 0, "Edge Root Rule Chain"),
"EDGE") "EDGE")
.getData().get(0).getId(); .getData().get(0).getId();
@ -210,7 +209,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected void updateRootRuleChainMetadata() throws Exception { protected void updateRootRuleChainMetadata() throws Exception {
RuleChainId rootRuleChainId = getEdgeRootRuleChainId(); RuleChainId rootRuleChainId = getEdgeRootRuleChainId();
RuleChainMetaData rootRuleChainMetadata = doGet("/api/ruleChain/" + rootRuleChainId.getId().toString() + "/metadata", RuleChainMetaData.class); RuleChainMetaData rootRuleChainMetadata = doGet("/api/ruleChain/" + rootRuleChainId.getId().toString() + "/metadata", RuleChainMetaData.class);
rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugMode(random.nextBoolean())); rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugSettings(DebugSettings.all()));
doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class); doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class);
} }

4
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -21,6 +21,7 @@ import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.rule.engine.util.TbMsgSource;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
@ -38,6 +39,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -228,7 +230,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
// update metadata for root rule chain // update metadata for root rule chain
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
metaData.getNodes().forEach(n -> n.setDebugMode(true)); metaData.getNodes().forEach(n -> n.setDebugSettings(DebugSettings.all()));
doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class);

11
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
@ -144,7 +145,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true); ruleNode1.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setFetchTo(TbMsgSource.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
@ -154,7 +155,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true); ruleNode2.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setFetchTo(TbMsgSource.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@ -250,7 +251,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true); ruleNode1.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setFetchTo(TbMsgSource.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
@ -259,7 +260,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode12 = new RuleNode(); RuleNode ruleNode12 = new RuleNode();
ruleNode12.setName("Simple Rule Node 1"); ruleNode12.setName("Simple Rule Node 1");
ruleNode12.setType(org.thingsboard.rule.engine.flow.TbRuleChainInputNode.class.getName()); ruleNode12.setType(org.thingsboard.rule.engine.flow.TbRuleChainInputNode.class.getName());
ruleNode12.setDebugMode(true); ruleNode12.setDebugSettings(DebugSettings.all());
TbRuleChainInputNodeConfiguration configuration12 = new TbRuleChainInputNodeConfiguration(); TbRuleChainInputNodeConfiguration configuration12 = new TbRuleChainInputNodeConfiguration();
configuration12.setRuleChainId(secondaryRuleChain.getId().getId().toString()); configuration12.setRuleChainId(secondaryRuleChain.getId().getId().toString());
ruleNode12.setConfiguration(JacksonUtil.valueToTree(configuration12)); ruleNode12.setConfiguration(JacksonUtil.valueToTree(configuration12));
@ -284,7 +285,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true); ruleNode2.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setFetchTo(TbMsgSource.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));

3
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
@ -97,7 +98,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
ruleNode.setName("Simple Rule Node"); ruleNode.setName("Simple Rule Node");
ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode.setDebugMode(true); ruleNode.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
configuration.setFetchTo(TbMsgSource.METADATA); configuration.setFetchTo(TbMsgSource.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey")); configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));

5
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.alarm.EntityAlarm;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.event.LifecycleEvent; import org.thingsboard.server.common.data.event.LifecycleEvent;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
@ -496,7 +497,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true); ruleNode1.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@ -505,7 +506,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true); ruleNode2.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));

4
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -471,7 +471,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
RuleNode ruleNode1 = new RuleNode(); RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Generator 1"); ruleNode1.setName("Generator 1");
ruleNode1.setType(TbMsgGeneratorNode.class.getName()); ruleNode1.setType(TbMsgGeneratorNode.class.getName());
ruleNode1.setDebugMode(true);
TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration(); TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration();
configuration1.setOriginatorType(originatorId.getEntityType()); configuration1.setOriginatorType(originatorId.getEntityType());
configuration1.setOriginatorId(originatorId.getId().toString()); configuration1.setOriginatorId(originatorId.getId().toString());
@ -481,7 +480,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
@ -510,7 +508,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@ -519,7 +516,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));

9
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
@ -870,7 +871,7 @@ public class VersionControlTest extends AbstractControllerTest {
RuleNode ruleNode1 = new RuleNode(); RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Generator 1"); ruleNode1.setName("Generator 1");
ruleNode1.setType(TbMsgGeneratorNode.class.getName()); ruleNode1.setType(TbMsgGeneratorNode.class.getName());
ruleNode1.setDebugMode(true); ruleNode1.setDebugSettings(DebugSettings.all());
TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration(); TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration();
configuration1.setOriginatorType(originatorId.getEntityType()); configuration1.setOriginatorType(originatorId.getEntityType());
configuration1.setOriginatorId(originatorId.getId().toString()); configuration1.setOriginatorId(originatorId.getId().toString());
@ -880,7 +881,7 @@ public class VersionControlTest extends AbstractControllerTest {
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true); ruleNode2.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
@ -908,7 +909,7 @@ public class VersionControlTest extends AbstractControllerTest {
ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true); ruleNode1.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@ -917,7 +918,7 @@ public class VersionControlTest extends AbstractControllerTest {
ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true); ruleNode2.setDebugSettings(DebugSettings.all());
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));

1
common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.tenant; package org.thingsboard.server.dao.tenant;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;

32
common/data/src/main/java/org/thingsboard/server/common/data/HasDebugSettings.java

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

2
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -32,4 +32,6 @@ public class SystemParams {
long maxDatapointsLimit; long maxDatapointsLimit;
long maxResourceSize; long maxResourceSize;
boolean mobileQrEnabled; boolean mobileQrEnabled;
int maxDebugModeDurationMinutes;
String ruleChainDebugPerTenantLimitsConfiguration;
} }

59
common/data/src/main/java/org/thingsboard/server/common/data/debug/DebugSettings.java

@ -0,0 +1,59 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.debug;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class DebugSettings {
private static DebugSettings DEBUG_OFF = new DebugSettings(false, 0);
private static DebugSettings DEBUG_FAILURES = new DebugSettings(true, 0);
public DebugSettings(boolean failuresEnabled, long allEnabledUntil) {
this.failuresEnabled = failuresEnabled;
this.allEnabled = false;
this.allEnabledUntil = allEnabledUntil;
}
@Schema(description = "Debug failures. ", example = "false")
private boolean failuresEnabled;
@Schema(description = "Debug All. Used as a trigger for updating debugAllUntil.", example = "false")
private boolean allEnabled;
@Schema(description = "Timestamp of the end time for the processing debug events.")
private long allEnabledUntil;
public static DebugSettings off() {return DebugSettings.DEBUG_OFF;}
public static DebugSettings failures() {return DebugSettings.DEBUG_FAILURES;}
public static DebugSettings until(long ts) {return new DebugSettings(false, ts);}
public static DebugSettings failuresOrUntil(long ts) {return new DebugSettings(true, ts);}
public static DebugSettings all() {
var ds = new DebugSettings();
ds.setAllEnabled(true);
return ds;
}
public DebugSettings copy(long maxDebugAllUntil) {
return new DebugSettings(failuresEnabled, allEnabled ? maxDebugAllUntil : Math.min(allEnabledUntil, maxDebugAllUntil));
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java

@ -23,9 +23,13 @@ public final class TbNodeConnectionType {
public static final String SUCCESS = "Success"; public static final String SUCCESS = "Success";
public static final String FAILURE = "Failure"; public static final String FAILURE = "Failure";
public static final String ACK = "ACK";
public static final String TRUE = "True"; public static final String TRUE = "True";
public static final String FALSE = "False"; public static final String FALSE = "False";
public static final String OTHER = "Other"; public static final String OTHER = "Other";
public static final String TO_ROOT_RULE_CHAIN = "To Root Rule Chain";
} }

35
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java

@ -16,13 +16,17 @@
package org.thingsboard.server.common.data.rule; package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
@ -32,7 +36,8 @@ import org.thingsboard.server.common.data.validation.NoXss;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements HasName { @JsonIgnoreProperties(ignoreUnknown = true)
public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements HasName, HasDebugSettings {
private static final long serialVersionUID = -5656679015121235465L; private static final long serialVersionUID = -5656679015121235465L;
@ -45,15 +50,18 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
@Length(fieldName = "name") @Length(fieldName = "name")
@Schema(description = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading") @Schema(description = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading")
private String name; private String name;
@Schema(description = "Enable/disable debug. ", example = "false") @Deprecated
@Schema(description = "Enable/disable debug. ", example = "false", deprecated = true)
private boolean debugMode; private boolean debugMode;
@Schema(description = "Debug settings object.")
private DebugSettings debugSettings;
@Schema(description = "Enable/disable singleton mode. ", example = "false") @Schema(description = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode; private boolean singletonMode;
@Schema(description = "Queue name. ", example = "Main") @Schema(description = "Queue name. ", example = "Main")
private String queueName; private String queueName;
@Schema(description = "Version of rule node configuration. ", example = "0") @Schema(description = "Version of rule node configuration. ", example = "0")
private int configurationVersion; private int configurationVersion;
@Schema(description = "JSON with the rule node configuration. Structure depends on the rule node implementation.", implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "JSON with the rule node configuration. Structure depends on the rule node implementation.", implementation = JsonNode.class)
private transient JsonNode configuration; private transient JsonNode configuration;
@JsonIgnore @JsonIgnore
private byte[] configurationBytes; private byte[] configurationBytes;
@ -73,7 +81,7 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
this.ruleChainId = ruleNode.getRuleChainId(); this.ruleChainId = ruleNode.getRuleChainId();
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugSettings = ruleNode.getDebugSettings();
this.singletonMode = ruleNode.isSingletonMode(); this.singletonMode = ruleNode.isSingletonMode();
this.setConfiguration(ruleNode.getConfiguration()); this.setConfiguration(ruleNode.getConfiguration());
this.externalId = ruleNode.getExternalId(); this.externalId = ruleNode.getExternalId();
@ -93,9 +101,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
} }
@Schema(description = "JSON object with the Rule Node Id. " + @Schema(description = "JSON object with the Rule Node Id. " +
"Specify this field to update the Rule Node. " + "Specify this field to update the Rule Node. " +
"Referencing non-existing Rule Node Id will cause error. " + "Referencing non-existing Rule Node Id will cause error. " +
"Omit this field to create new rule node.") "Omit this field to create new rule node.")
@Override @Override
public RuleNodeId getId() { public RuleNodeId getId() {
return super.getId(); return super.getId();
@ -107,10 +115,21 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
return super.getCreatedTime(); return super.getCreatedTime();
} }
@Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = JsonNode.class)
@Override @Override
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();
} }
// Getter is ignored for serialization
@JsonIgnore
public boolean isDebugMode() {
return debugMode;
}
// Setter is annotated for deserialization
@JsonSetter
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -95,6 +95,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxDPStorageDays; private long maxDPStorageDays;
@Schema(example = "50") @Schema(example = "50")
private int maxRuleNodeExecutionsPerMessage; private int maxRuleNodeExecutionsPerMessage;
@Schema(example = "15")
private int maxDebugModeDurationMinutes;
@Schema(example = "0") @Schema(example = "0")
private long maxEmails; private long maxEmails;
@Schema(example = "true") @Schema(example = "true")

3
common/edge-api/src/main/proto/edge.proto

@ -169,11 +169,12 @@ message RuleNodeProto {
int64 idLSB = 2; int64 idLSB = 2;
string type = 3; string type = 3;
string name = 4; string name = 4;
bool debugMode = 5; bool debugMode = 5 [deprecated = true];
string configuration = 6; string configuration = 6;
string additionalInfo = 7; string additionalInfo = 7;
bool singletonMode = 8; bool singletonMode = 8;
int32 configurationVersion = 9; int32 configurationVersion = 9;
string debugSettings = 10;
} }
message NodeConnectionInfoProto { message NodeConnectionInfoProto {

60
common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.common.util;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import java.util.Set;
public final class DebugModeUtil {
private static final int DEBUG_MODE_DEFAULT_DURATION_MINUTES = 15;
private DebugModeUtil() {
}
public static int getMaxDebugAllDuration(int tenantProfileDuration, int systemDefaultDuration) {
if (tenantProfileDuration > 0) {
return tenantProfileDuration;
} else {
return systemDefaultDuration > 0 ? systemDefaultDuration : DEBUG_MODE_DEFAULT_DURATION_MINUTES;
}
}
public static boolean isDebugAllAvailable(HasDebugSettings debugSettingsAware) {
var debugSettings = debugSettingsAware.getDebugSettings();
return debugSettings != null && debugSettings.getAllEnabledUntil() > System.currentTimeMillis();
}
public static boolean isDebugAvailable(HasDebugSettings debugSettingsAware, String nodeConnection) {
if (isDebugAllAvailable(debugSettingsAware)) {
return true;
} else {
var debugSettings = debugSettingsAware.getDebugSettings();
return debugSettings != null && debugSettings.isFailuresEnabled() && TbNodeConnectionType.FAILURE.equals(nodeConnection);
}
}
public static boolean isDebugFailuresAvailable(HasDebugSettings debugSettingsAware, Set<String> nodeConnections) {
if (isDebugAllAvailable(debugSettingsAware)) {
return true;
} else {
var debugSettings = debugSettingsAware.getDebugSettings();
return debugSettings != null && nodeConnections != null && debugSettings.isFailuresEnabled() && nodeConnections.contains(TbNodeConnectionType.FAILURE);
}
}
}

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

@ -18,10 +18,14 @@ package org.thingsboard.server.dao.entity;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -33,11 +37,13 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.housekeeper.CleanUpService; import org.thingsboard.server.dao.housekeeper.CleanUpService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
public abstract class AbstractEntityService { public abstract class AbstractEntityService {
@ -68,6 +74,13 @@ public abstract class AbstractEntityService {
@Lazy @Lazy
protected CleanUpService cleanUpService; protected CleanUpService cleanUpService;
@Autowired
@Lazy
private TbTenantProfileCache tbTenantProfileCache;
@Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes;
protected void createRelation(TenantId tenantId, EntityRelation relation) { protected void createRelation(TenantId tenantId, EntityRelation relation) {
log.debug("Creating relation: {}", relation); log.debug("Creating relation: {}", relation);
relationService.saveRelation(tenantId, relation); relationService.saveRelation(tenantId, relation);
@ -124,4 +137,17 @@ public abstract class AbstractEntityService {
} }
} }
} }
protected void updateDebugSettings(TenantId tenantId, HasDebugSettings entity, long now) {
if (entity.getDebugSettings() != null) {
entity.setDebugSettings(entity.getDebugSettings().copy(getMaxDebugAllUntil(tenantId, now)));
} else if (entity.isDebugMode()) {
entity.setDebugSettings(DebugSettings.failuresOrUntil(getMaxDebugAllUntil(tenantId, now)));
entity.setDebugMode(false);
}
}
private long getMaxDebugAllUntil(TenantId tenantId, long now) {
return now + TimeUnit.MINUTES.toMillis(DebugModeUtil.getMaxDebugAllDuration(tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes));
}
} }

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

@ -398,6 +398,7 @@ public class ModelConstants {
public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message"; public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message";
public static final String DEBUG_MODE = "debug_mode"; public static final String DEBUG_MODE = "debug_mode";
public static final String DEBUG_SETTINGS = "debug_settings";
public static final String SINGLETON_MODE = "singleton_mode"; public static final String SINGLETON_MODE = "singleton_mode";
public static final String QUEUE_NAME = "queue_name"; public static final String QUEUE_NAME = "queue_name";

11
dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java

@ -22,6 +22,9 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
@ -58,8 +61,8 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
@Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo; private JsonNode additionalInfo;
@Column(name = ModelConstants.DEBUG_MODE) @Column(name = ModelConstants.DEBUG_SETTINGS)
private boolean debugMode; private String debugSettings;
@Column(name = ModelConstants.SINGLETON_MODE) @Column(name = ModelConstants.SINGLETON_MODE)
private boolean singletonMode; private boolean singletonMode;
@ -83,7 +86,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
} }
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugSettings = JacksonUtil.toString(ruleNode.getDebugSettings());
this.singletonMode = ruleNode.isSingletonMode(); this.singletonMode = ruleNode.isSingletonMode();
this.queueName = ruleNode.getQueueName(); this.queueName = ruleNode.getQueueName();
this.configurationVersion = ruleNode.getConfigurationVersion(); this.configurationVersion = ruleNode.getConfigurationVersion();
@ -103,7 +106,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
} }
ruleNode.setType(type); ruleNode.setType(type);
ruleNode.setName(name); ruleNode.setName(name);
ruleNode.setDebugMode(debugMode); ruleNode.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
ruleNode.setSingletonMode(singletonMode); ruleNode.setSingletonMode(singletonMode);
ruleNode.setQueueName(queueName); ruleNode.setQueueName(queueName);
ruleNode.setConfigurationVersion(configurationVersion); ruleNode.setConfigurationVersion(configurationVersion);

5
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -214,9 +214,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
} }
RuleChainId ruleChainId = ruleChain.getId(); RuleChainId ruleChainId = ruleChain.getId();
if (nodes != null) { if (nodes != null) {
long now = System.currentTimeMillis();
for (RuleNode node : toAddOrUpdate) { for (RuleNode node : toAddOrUpdate) {
node.setRuleChainId(ruleChainId); node.setRuleChainId(ruleChainId);
node = ruleNodeUpdater.apply(node); node = ruleNodeUpdater.apply(node);
updateDebugSettings(tenantId, node, now);
RuleChainDataValidator.validateRuleNode(node); RuleChainDataValidator.validateRuleNode(node);
RuleNode savedNode = ruleNodeDao.save(tenantId, node); RuleNode savedNode = ruleNodeDao.save(tenantId, node);
relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(),
@ -261,7 +265,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
layout.remove("description"); layout.remove("description");
layout.remove("ruleChainNodeId"); layout.remove("ruleChainNodeId");
targetNode.setAdditionalInfo(layout); targetNode.setAdditionalInfo(layout);
targetNode.setDebugMode(false);
targetNode = ruleNodeDao.save(tenantId, targetNode); targetNode = ruleNodeDao.save(tenantId, targetNode);
EntityRelation sourceRuleChainToRuleNode = new EntityRelation(); EntityRelation sourceRuleChainToRuleNode = new EntityRelation();

2
dao/src/main/resources/sql/schema-entities.sql

@ -182,7 +182,7 @@ CREATE TABLE IF NOT EXISTS rule_node (
configuration varchar(10000000), configuration varchar(10000000),
type varchar(255), type varchar(255),
name varchar(255), name varchar(255),
debug_mode boolean, debug_settings varchar(1024),
singleton_mode boolean, singleton_mode boolean,
queue_name varchar(255), queue_name varchar(255),
external_id uuid external_id uuid

18
monitoring/src/main/resources/root_rule_chain.json

@ -20,7 +20,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries", "name": "Save Timeseries",
"debugMode": true,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -35,7 +34,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Attributes", "name": "Save Attributes",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 1, "configurationVersion": 1,
"configuration": { "configuration": {
@ -53,7 +51,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "Message Type Switch", "name": "Message Type Switch",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -68,7 +65,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -85,7 +81,6 @@
}, },
"type": "org.thingsboard.rule.engine.action.TbLogNode", "type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log Other", "name": "Log Other",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -102,7 +97,6 @@
}, },
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"name": "RPC Call Request", "name": "RPC Call Request",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -117,7 +111,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode", "type": "org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode",
"name": "Is Entity Group", "name": "Is Entity Group",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -134,7 +127,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode",
"name": "Post attributes or RPC request", "name": "Post attributes or RPC request",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -152,7 +144,6 @@
}, },
"type": "org.thingsboard.rule.engine.transform.TbDuplicateMsgToGroupNode", "type": "org.thingsboard.rule.engine.transform.TbDuplicateMsgToGroupNode",
"name": "Duplicate To Group Entities", "name": "Duplicate To Group Entities",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -169,7 +160,6 @@
}, },
"type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"name": "Device Profile Node", "name": "Device Profile Node",
"debugMode": true,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -186,7 +176,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Test JS script", "name": "Test JS script",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -204,7 +193,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Test TBEL script", "name": "Test TBEL script",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -222,7 +210,6 @@
}, },
"type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
"name": "Add arrival timestamp", "name": "Add arrival timestamp",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -240,7 +227,6 @@
}, },
"type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
"name": "Calculate additional latencies", "name": "Calculate additional latencies",
"debugMode": true,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -258,7 +244,6 @@
}, },
"type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode", "type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode",
"name": "To latencies asset", "name": "To latencies asset",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -287,7 +272,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries", "name": "Save Timeseries",
"debugMode": true,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -303,7 +287,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode", "type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode",
"name": "Has testData", "name": "Has testData",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -323,7 +306,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries with TTL", "name": "Save Timeseries with TTL",
"debugMode": true,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {

3
msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json

@ -9,7 +9,6 @@
}, },
"type": "org.thingsboard.rule.engine.mqtt.TbMqttNode", "type": "org.thingsboard.rule.engine.mqtt.TbMqttNode",
"name": "test mqtt", "name": "test mqtt",
"debugMode": true,
"singletonMode": true, "singletonMode": true,
"queueName": "HighPriority", "queueName": "HighPriority",
"configurationVersion": 0, "configurationVersion": 0,
@ -36,7 +35,6 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "save timeseries", "name": "save timeseries",
"debugMode": true,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {
@ -54,7 +52,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "switch", "name": "switch",
"debugMode": false,
"singletonMode": false, "singletonMode": false,
"configurationVersion": 0, "configurationVersion": 0,
"configuration": { "configuration": {

3
msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json

@ -8,7 +8,6 @@
}, },
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "msgTypeSwitch", "name": "msgTypeSwitch",
"debugMode": true,
"configuration": { "configuration": {
"version": 0 "version": 0
} }
@ -20,7 +19,6 @@
}, },
"type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
"name": "formResponse", "name": "formResponse",
"debugMode": true,
"configuration": { "configuration": {
"jsScript": "if (msg.method == \"getResponse\") {\n return {msg: {\"response\": \"requestReceived\"}, metadata: metadata, msgType: msgType};\n}\n\nreturn {msg: msg, metadata: metadata, msgType: msgType};" "jsScript": "if (msg.method == \"getResponse\") {\n return {msg: {\"response\": \"requestReceived\"}, metadata: metadata, msgType: msgType};\n}\n\nreturn {msg: msg, metadata: metadata, msgType: msgType};"
} }
@ -32,7 +30,6 @@
}, },
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode", "type": "org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode",
"name": "rpcReply", "name": "rpcReply",
"debugMode": true,
"configuration": { "configuration": {
"requestIdMetaDataAttribute": "requestId" "requestIdMetaDataAttribute": "requestId"
} }

2
ui-ngx/src/app/core/auth/auth.models.ts

@ -27,6 +27,8 @@ export interface SysParamsState {
mobileQrEnabled: boolean; mobileQrEnabled: boolean;
userSettings: UserSettings; userSettings: UserSettings;
maxResourceSize: number; maxResourceSize: number;
maxDebugModeDurationMinutes: number;
ruleChainDebugPerTenantLimitsConfiguration?: string;
} }
export interface SysParams extends SysParamsState { export interface SysParams extends SysParamsState {

1
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -31,6 +31,7 @@ const emptyUserAuthState: AuthPayload = {
persistDeviceStateToTelemetry: false, persistDeviceStateToTelemetry: false,
mobileQrEnabled: false, mobileQrEnabled: false,
maxResourceSize: 0, maxResourceSize: 0,
maxDebugModeDurationMinutes: 0,
userSettings: initialUserSettings userSettings: initialUserSettings
}; };

32
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html

@ -0,0 +1,32 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button mat-stroked-button
class="tb-rounded-btn w-36 flex-1"
color="primary"
#matButton
[class.active]="((isDebugAllActive$ | async) || failuresEnabled) && !disabled"
[disabled]="disabled"
(click)="openDebugStrategyPanel($event, matButton)">
<mat-icon [color]="debugSettingsFormGroup.disabled ? 'inherit' : 'primary'">bug_report</mat-icon>
<span *ngIf="!failuresEnabled && !(isDebugAllActive$ | async)" translate>common.disabled</span>
<span *ngIf="(isDebugAllActive$ | async) && failuresEnabled" translate>debug-config.all</span>
<span *ngIf="(isDebugAllActive$ | async) && !failuresEnabled">
{{ !allEnabled ? (allEnabledUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) }}
</span>
<span *ngIf="!(isDebugAllActive$ | async) && failuresEnabled" translate>debug-config.failures</span>
</button>

155
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts

@ -0,0 +1,155 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { TbPopoverService } from '@shared/components/popover.service';
import { MatButton } from '@angular/material/button';
import { DebugSettingsPanelComponent } from './debug-settings-panel.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { of, shareReplay, timer } from 'rxjs';
import { SECOND } from '@shared/models/time/time.models';
import { DebugSettings } from '@shared/models/entity.models';
import { map, startWith, switchMap, takeWhile } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'tb-debug-settings-button',
templateUrl: './debug-settings-button.component.html',
standalone: true,
imports: [
CommonModule,
SharedModule,
DurationLeftPipe,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DebugSettingsButtonComponent),
multi: true
},
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugSettingsButtonComponent implements ControlValueAccessor {
@Input() debugLimitsConfiguration: string;
debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false],
allEnabled: [false],
allEnabledUntil: []
});
disabled = false;
isDebugAllActive$ = this.debugSettingsFormGroup.get('allEnabled').valueChanges.pipe(
startWith(null),
switchMap(() => {
if (this.allEnabled) {
return of(true);
} else {
return timer(0, SECOND).pipe(
map(() => this.allEnabledUntil > new Date().getTime()),
takeWhile(value => value, true)
);
}
}),
takeUntilDestroyed(),
shareReplay(1)
);
readonly maxDebugModeDurationMinutes = getCurrentAuthState(this.store).maxDebugModeDurationMinutes;
private propagateChange: (settings: DebugSettings) => void = () => {};
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private store: Store<AppState>,
private viewContainerRef: ViewContainerRef,
private fb: FormBuilder,
) {
this.debugSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => {
this.propagateChange(value);
})
}
get failuresEnabled(): boolean {
return this.debugSettingsFormGroup.get('failuresEnabled').value;
}
get allEnabled(): boolean {
return this.debugSettingsFormGroup.get('allEnabled').value;
}
get allEnabledUntil(): number {
return this.debugSettingsFormGroup.get('allEnabledUntil').value;
}
openDebugStrategyPanel($event: Event, matButton: MatButton): void {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
const debugSettings = this.debugSettingsFormGroup.value;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DebugSettingsPanelComponent, 'bottom', true, null,
{
...debugSettings,
maxDebugModeDurationMinutes: this.maxDebugModeDurationMinutes,
debugLimitsConfiguration: this.debugLimitsConfiguration
},
{},
{}, {}, true);
debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: DebugSettings) => {
this.debugSettingsFormGroup.patchValue(settings);
debugStrategyPopover.hide();
});
}
}
registerOnChange(fn: (settings: DebugSettings) => void): void {
this.propagateChange = fn;
}
registerOnTouched(_: () => void): void {}
writeValue(settings: DebugSettings): void {
this.debugSettingsFormGroup.patchValue(settings, {emitEvent: false});
this.debugSettingsFormGroup.get('allEnabled').updateValueAndValidity({onlySelf: true});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.debugSettingsFormGroup.disable({emitEvent: false});
} else {
this.debugSettingsFormGroup.enable({emitEvent: false});
}
}
}

66
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.html

@ -0,0 +1,66 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="flex max-w-sm flex-col gap-3 p-2">
<div class="tb-form-panel-title" translate>debug-config.label</div>
<div class="hint-container">
<div class="tb-form-hint tb-primary-fill tb-flex center">
@if (debugLimitsConfiguration) {
{{ 'debug-config.hint.main-limited' | translate: { msg: maxMessagesCount, sec: maxTimeFrameSec } }}
} @else {
{{ 'debug-config.hint.main' | translate }}
}
</div>
</div>
<div class="flex flex-col gap-3">
<mat-slide-toggle class="mat-slide" [formControl]="onFailuresControl">
<div tb-hint-tooltip-icon="{{ 'debug-config.hint.on-failure' | translate }}">
{{ 'debug-config.on-failure' | translate }}
</div>
</mat-slide-toggle>
<div class="align-center flex justify-between">
<mat-slide-toggle class="mat-slide" [formControl]="debugAllControl">
<div tb-hint-tooltip-icon="{{ 'debug-config.hint.all-messages' | translate }}">
{{ 'debug-config.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) } }}
</div>
</mat-slide-toggle>
<button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !allEnabled"
class="tb-mat-20"
matTooltip="{{ 'action.reset' | translate }}"
matTooltipPosition="above"
color="primary"
(click)="onReset()">
<mat-icon class="material-icons">refresh</mat-icon>
</button>
</div>
</div>
<div class="flex justify-end">
<button mat-button
color="primary"
type="button"
(click)="onCancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="onApply()">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

134
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts

@ -0,0 +1,134 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
booleanAttribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { FormBuilder } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { SECOND } from '@shared/models/time/time.models';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { of, shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DebugSettings } from '@shared/models/entity.models';
import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators';
@Component({
selector: 'tb-debug-settings-panel',
templateUrl: './debug-settings-panel.component.html',
standalone: true,
imports: [
SharedModule,
CommonModule,
DurationLeftPipe
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugSettingsPanelComponent extends PageComponent implements OnInit {
@Input() popover: TbPopoverComponent<DebugSettingsPanelComponent>;
@Input({ transform: booleanAttribute }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false;
@Input() allEnabledUntil = 0;
@Input() maxDebugModeDurationMinutes: number;
@Input() debugLimitsConfiguration: string;
onFailuresControl = this.fb.control(false);
debugAllControl = this.fb.control(false);
maxMessagesCount: string;
maxTimeFrameSec: string;
initialAllEnabled: boolean;
isDebugAllActive$ = this.debugAllControl.valueChanges.pipe(
startWith(this.debugAllControl.value),
switchMap(value => {
if (value) {
return of(true);
} else {
return timer(0, SECOND).pipe(
map(() => this.allEnabledUntil > new Date().getTime()),
takeWhile(value => value, true)
);
}
}),
takeUntilDestroyed(),
shareReplay(1),
);
onSettingsApplied = new EventEmitter<DebugSettings>();
constructor(private fb: FormBuilder,
private cd: ChangeDetectorRef) {
super();
this.debugAllControl.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => {
this.allEnabled = value;
this.cd.markForCheck();
});
this.isDebugAllActive$.pipe(
distinctUntilChanged(),
takeUntilDestroyed()
).subscribe(isDebugOn => this.debugAllControl.patchValue(isDebugOn, {emitEvent: false}))
}
ngOnInit(): void {
this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0];
this.maxTimeFrameSec = this.debugLimitsConfiguration?.split(':')[1];
this.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled);
this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime();
}
onCancel(): void {
this.popover?.hide();
}
onApply(): void {
const isDebugAllChanged = this.initialAllEnabled !== this.debugAllControl.value || this.initialAllEnabled !== this.allEnabledUntil > new Date().getTime();
if (isDebugAllChanged) {
this.onSettingsApplied.emit({
allEnabled: this.allEnabled,
failuresEnabled: this.onFailuresControl.value,
allEnabledUntil: 0,
});
} else {
this.onSettingsApplied.emit({
allEnabled: false,
failuresEnabled: this.onFailuresControl.value,
allEnabledUntil: this.allEnabledUntil,
});
}
}
onReset(): void {
this.debugAllControl.patchValue(true);
this.allEnabledUntil = 0;
this.cd.markForCheck();
}
}

12
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -40,8 +40,8 @@ import {
EventContentDialogData EventContentDialogData
} from '@home/components/event/event-content-dialog.component'; } from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils'; import { isEqual, sortObjectKeys } from '@core/utils';
import { historyInterval, MINUTE } from '@shared/models/time/time.models'; import { DAY, historyInterval, MINUTE } from '@shared/models/time/time.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { import {
@ -51,6 +51,9 @@ import {
FilterEntityColumn FilterEntityColumn
} from '@home/components/event/event-filter-panel.component'; } from '@home/components/event/event-filter-panel.component';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> { export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
@ -59,6 +62,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private filterParams: FilterEventBody = {}; private filterParams: FilterEventBody = {};
private filterColumns: FilterEntityColumn[] = []; private filterColumns: FilterEntityColumn[] = [];
private readonly maxDebugModeDurationMinutes = getCurrentAuthState(this.store).maxDebugModeDurationMinutes;
set eventType(eventType: EventType | DebugEventType) { set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) { if (this.eventTypeValue !== eventType) {
@ -88,13 +92,15 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private store: Store<AppState>,
public testButtonLabel?: string, public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) { private debugEventSelected?: EventEmitter<EventBody>) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
this.useTimePageLink = true; this.useTimePageLink = true;
this.defaultTimewindowInterval = historyInterval(MINUTE * 15); const defaultInterval = this.maxDebugModeDurationMinutes ? Math.min(this.maxDebugModeDurationMinutes * MINUTE, DAY) : DAY;
this.defaultTimewindowInterval = historyInterval(defaultInterval);
this.detailsPanelEnabled = false; this.detailsPanelEnabled = false;
this.selectionEnabled = false; this.selectionEnabled = false;
this.searchEnabled = false; this.searchEnabled = false;

4
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -36,6 +36,8 @@ import { DebugEventType, EventBody, EventType } from '@shared/models/event.model
import { Overlay } from '@angular/cdk/overlay'; import { Overlay } from '@angular/cdk/overlay';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({ @Component({
selector: 'tb-event-table', selector: 'tb-event-table',
@ -123,6 +125,7 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
private dialog: MatDialog, private dialog: MatDialog,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private store: Store<AppState>,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
} }
@ -142,6 +145,7 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.overlay, this.overlay,
this.viewContainerRef, this.viewContainerRef,
this.cd, this.cd,
this.store,
this.functionTestButtonLabel, this.functionTestButtonLabel,
this.debugEventSelected this.debugEventSelected
); );

19
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -370,6 +370,25 @@
</div> </div>
</fieldset> </fieldset>
<fieldset class="fields-group">
<legend class="group-title">
{{ 'tenant-profile.debug' | translate }}
</legend>
<div class="fields-element flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.maximum-debug-duration-min</mat-label>
<input matInput min="0" step="1"
formControlName="maxDebugModeDurationMinutes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDebugModeDurationMinutes').hasError('min')">
{{ 'tenant-profile.maximum-debug-duration-min-range' | translate }}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div class="flex-1"></div>
</div>
</fieldset>
<fieldset class="fields-group"> <fieldset class="fields-group">
<legend class="group-title"> <legend class="group-title">
{{ 'tenant-profile.ota-files-in-bytes' | translate }} <span translate>tenant-profile.unlimited</span> {{ 'tenant-profile.ota-files-in-bytes' | translate }} <span translate>tenant-profile.unlimited</span>

1
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -86,6 +86,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
tenantNotificationRequestsRateLimit: [null, []], tenantNotificationRequestsRateLimit: [null, []],
tenantNotificationRequestsPerRuleRateLimit: [null, []], tenantNotificationRequestsPerRuleRateLimit: [null, []],
maxTransportMessages: [null, [Validators.required, Validators.min(0)]], maxTransportMessages: [null, [Validators.required, Validators.min(0)]],
maxDebugModeDurationMinutes: [null, [Validators.min(0)]],
maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]],
maxREExecutions: [null, [Validators.required, Validators.min(0)]], maxREExecutions: [null, [Validators.required, Validators.min(0)]],
maxJSExecutions: [null, [Validators.required, Validators.min(0)]], maxJSExecutions: [null, [Validators.required, Validators.min(0)]],

26
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -22,7 +22,7 @@
</button> </button>
</div> </div>
<form [formGroup]="ruleNodeFormGroup" class="mat-padding"> <form [formGroup]="ruleNodeFormGroup" class="mat-padding">
<section class="title-row"> <section class="title-row flex align-center">
<mat-form-field class="mat-block flex-1"> <mat-form-field class="mat-block flex-1">
<mat-label translate>rulenode.name</mat-label> <mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required> <input matInput formControlName="name" required>
@ -34,13 +34,23 @@
{{ 'rulenode.name-max-length' | translate }} {{ 'rulenode.name-max-length' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<section class="node-setting"> <section class="mb-5 flex max-w-xs flex-row">
<mat-slide-toggle formControlName="debugMode"> <tb-debug-settings-button
{{ 'rulenode.debug-mode' | translate }} class="mr-2"
</mat-slide-toggle> formControlName="debugSettings"
<mat-slide-toggle *ngIf="isSingleton()" formControlName="singletonMode"> [debugLimitsConfiguration]="ruleChainDebugPerTenantLimitsConfiguration"
{{ 'rulenode.singleton-mode' | translate }} />
</mat-slide-toggle> <button mat-stroked-button
class="tb-rounded-btn flex-1"
color="primary"
*ngIf="isSingleton()"
[disabled]="ruleNodeFormGroup.get('singletonMode').disabled || disabled"
[class.active]="ruleNodeFormGroup.get('singletonMode').value"
(click)="onSingleModeChange($event)">
<mat-icon [class.invisible]="!ruleNodeFormGroup.get('singletonMode').value">checkmark</mat-icon>
<span translate>rulenode.singleton</span>
<input class="hidden" formControlName="singletonMode">
</button>
</section> </section>
</section> </section>
<tb-rule-node-config #ruleNodeConfigComponent <tb-rule-node-config #ruleNodeConfigComponent

7
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss

@ -23,13 +23,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.node-setting {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 22px;
}
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;

14
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -39,6 +39,7 @@ import { ComponentClusteringMode } from '@shared/models/component-descriptor.mod
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { ServiceType } from '@shared/models/queue.models'; import { ServiceType } from '@shared/models/queue.models';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@Component({ @Component({
selector: 'tb-rule-node', selector: 'tb-rule-node',
@ -79,6 +80,8 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
ruleNodeFormGroup: UntypedFormGroup; ruleNodeFormGroup: UntypedFormGroup;
readonly ruleChainDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -92,7 +95,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
if (this.ruleNode) { if (this.ruleNode) {
this.ruleNodeFormGroup = this.fb.group({ this.ruleNodeFormGroup = this.fb.group({
name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]], name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]],
debugMode: [this.ruleNode.debugMode, []], debugSettings: [this.ruleNode.debugSettings],
singletonMode: [this.ruleNode.singletonMode, []], singletonMode: [this.ruleNode.singletonMode, []],
configuration: [this.ruleNode.configuration, [Validators.required]], configuration: [this.ruleNode.configuration, [Validators.required]],
additionalInfo: this.fb.group( additionalInfo: this.fb.group(
@ -167,6 +170,15 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
this.ruleNodeConfigComponent.validate(); this.ruleNodeConfigComponent.validate();
} }
onSingleModeChange($event: Event): void {
if ($event) {
$event.stopPropagation();
}
const singleModeControl = this.ruleNodeFormGroup.get('singletonMode');
singleModeControl.patchValue(!singleModeControl.value);
singleModeControl.markAsDirty();
}
openRuleChain($event: Event) { openRuleChain($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();

8
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -575,7 +575,7 @@ export class RuleChainPageComponent extends PageComponent
additionalInfo: ruleNode.additionalInfo, additionalInfo: ruleNode.additionalInfo,
configuration: ruleNode.configuration, configuration: ruleNode.configuration,
configurationVersion: isDefinedAndNotNull(ruleNode.configurationVersion) ? ruleNode.configurationVersion : 0, configurationVersion: isDefinedAndNotNull(ruleNode.configurationVersion) ? ruleNode.configurationVersion : 0,
debugMode: ruleNode.debugMode, debugSettings: ruleNode.debugSettings,
singletonMode: ruleNode.singletonMode, singletonMode: ruleNode.singletonMode,
queueName: ruleNode.queueName, queueName: ruleNode.queueName,
x: Math.round(ruleNode.additionalInfo.layoutX), x: Math.round(ruleNode.additionalInfo.layoutX),
@ -936,7 +936,7 @@ export class RuleChainPageComponent extends PageComponent
name: node.name, name: node.name,
configuration: deepClone(node.configuration), configuration: deepClone(node.configuration),
additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {}, additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {},
debugMode: node.debugMode, debugSettings: node.debugSettings,
singletonMode: node.singletonMode, singletonMode: node.singletonMode,
queueName: node.queueName queueName: node.queueName
}; };
@ -1009,7 +1009,6 @@ export class RuleChainPageComponent extends PageComponent
name: outputEdge.label, name: outputEdge.label,
configuration: {}, configuration: {},
additionalInfo: {}, additionalInfo: {},
debugMode: false,
singletonMode: false singletonMode: false
}; };
outputNode.additionalInfo.layoutX = Math.round(destNode.x); outputNode.additionalInfo.layoutX = Math.round(destNode.x);
@ -1055,7 +1054,6 @@ export class RuleChainPageComponent extends PageComponent
configuration: { configuration: {
ruleChainId: ruleChain.id.id ruleChainId: ruleChain.id.id
}, },
debugMode: false,
singletonMode: false, singletonMode: false,
x: Math.round(ruleChainNodeX), x: Math.round(ruleChainNodeX),
y: Math.round(ruleChainNodeY), y: Math.round(ruleChainNodeY),
@ -1475,7 +1473,7 @@ export class RuleChainPageComponent extends PageComponent
: node.component.configurationVersion, : node.component.configurationVersion,
configuration: node.configuration, configuration: node.configuration,
additionalInfo: node.additionalInfo ? node.additionalInfo : {}, additionalInfo: node.additionalInfo ? node.additionalInfo : {},
debugMode: node.debugMode, debugSettings: node.debugSettings,
singletonMode: node.singletonMode, singletonMode: node.singletonMode,
queueName: node.queueName queueName: node.queueName
}; };

6
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts

@ -32,6 +32,8 @@ import { RuleNodeDetailsComponent } from './rule-node-details.component';
import { RuleNodeLinkComponent } from './rule-node-link.component'; import { RuleNodeLinkComponent } from './rule-node-link.component';
import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component';
import { RuleNodeConfigComponent } from './rule-node-config.component'; import { RuleNodeConfigComponent } from './rule-node-config.component';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { DebugSettingsButtonComponent } from '@home/components/debug-settings/debug-settings-button.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -59,7 +61,9 @@ import { RuleNodeConfigComponent } from './rule-node-config.component';
CommonModule, CommonModule,
SharedModule, SharedModule,
HomeComponentsModule, HomeComponentsModule,
RuleChainRoutingModule RuleChainRoutingModule,
DurationLeftPipe,
DebugSettingsButtonComponent
] ]
}) })
export class RuleChainModule { } export class RuleChainModule { }

18
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -649,16 +649,20 @@ export class ImportExportService {
); );
} }
private processOldRuleChainConnections(ruleChainImport: RuleChainImport): Observable<RuleChainImport> { private processOldRuleChainConnections({ruleChain, metadata}: RuleChainImport): Observable<RuleChainImport> {
ruleChainImport.ruleChain = this.prepareImport(ruleChainImport.ruleChain); ruleChain = this.prepareImport(ruleChain);
const metadata = ruleChainImport.metadata; metadata = {
...metadata,
nodes: metadata.nodes.map(({ debugMode, ...node }: RuleNode & { debugMode: boolean }) => {
return debugMode ? { ...node, debugSettings: { failuresEnabled: true, allEnabled: true} } : node
})
};
if ((metadata as any).ruleChainConnections) { if ((metadata as any).ruleChainConnections) {
const ruleChainNameResolveObservables: Observable<void>[] = []; const ruleChainNameResolveObservables: Observable<void>[] = [];
for (const ruleChainConnection of (metadata as any).ruleChainConnections) { for (const ruleChainConnection of (metadata as any).ruleChainConnections) {
if (ruleChainConnection.targetRuleChainId && ruleChainConnection.targetRuleChainId.id) { if (ruleChainConnection.targetRuleChainId && ruleChainConnection.targetRuleChainId.id) {
const ruleChainNode: RuleNode = { const ruleChainNode: RuleNode = {
name: '', name: '',
debugMode: false,
singletonMode: false, singletonMode: false,
type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode', type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode',
configuration: { configuration: {
@ -686,13 +690,13 @@ export class ImportExportService {
} }
if (ruleChainNameResolveObservables.length) { if (ruleChainNameResolveObservables.length) {
return forkJoin(ruleChainNameResolveObservables).pipe( return forkJoin(ruleChainNameResolveObservables).pipe(
map(() => ruleChainImport) map(() => ({ruleChain, metadata}))
); );
} else { } else {
return of(ruleChainImport); return of({ruleChain, metadata});
} }
} else { } else {
return of(ruleChainImport); return of({ruleChain, metadata});
} }
} }

10
ui-ngx/src/app/shared/models/entity.models.ts

@ -193,4 +193,14 @@ export interface HasVersion {
version?: number; version?: number;
} }
export interface HasDebugSettings {
debugSettings?: DebugSettings;
}
export interface DebugSettings {
failuresEnabled?: boolean;
allEnabled?: boolean;
allEnabledUntil?: number;
}
export type VersionedEntity = EntityInfoData & HasVersion | RuleChainMetaData; export type VersionedEntity = EntityInfoData & HasVersion | RuleChainMetaData;

8
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -27,17 +27,16 @@ import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { TranslateService } from '@ngx-translate/core'; import { HasDebugSettings } from '@shared/models/entity.models';
export interface RuleNodeConfiguration { export interface RuleNodeConfiguration {
[key: string]: any; [key: string]: any;
} }
export interface RuleNode extends BaseData<RuleNodeId> { export interface RuleNode extends BaseData<RuleNodeId>, HasDebugSettings {
ruleChainId?: RuleChainId; ruleChainId?: RuleChainId;
type: string; type: string;
name: string; name: string;
debugMode: boolean;
singletonMode: boolean; singletonMode: boolean;
queueName?: string; queueName?: string;
configurationVersion?: number; configurationVersion?: number;
@ -332,7 +331,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
configurationDescriptor?: RuleNodeConfigurationDescriptor; configurationDescriptor?: RuleNodeConfigurationDescriptor;
} }
export interface FcRuleNodeType extends FcNode { export interface FcRuleNodeType extends FcNode, HasDebugSettings {
component?: RuleNodeComponentDescriptor; component?: RuleNodeComponentDescriptor;
singletonMode?: boolean; singletonMode?: boolean;
queueName?: string; queueName?: string;
@ -345,7 +344,6 @@ export interface FcRuleNode extends FcRuleNodeType {
ruleNodeId?: RuleNodeId; ruleNodeId?: RuleNodeId;
additionalInfo?: any; additionalInfo?: any;
configuration?: RuleNodeConfiguration; configuration?: RuleNodeConfiguration;
debugMode?: boolean;
error?: string; error?: string;
highlighted?: boolean; highlighted?: boolean;
componentClazz?: string; componentClazz?: string;

35
ui-ngx/src/app/shared/pipe/duration-left.pipe.ts

@ -0,0 +1,35 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MillisecondsToTimeStringPipe } from './milliseconds-to-time-string.pipe';
@Pipe({
name: 'durationLeft',
pure: false,
standalone: true,
})
export class DurationLeftPipe implements PipeTransform {
constructor(private translate: TranslateService, private millisecondsToTimeString: MillisecondsToTimeStringPipe) {
}
transform(untilTimestamp: number, shortFormat = true, onlyFirstDigit = true): string {
const time = this.millisecondsToTimeString.transform((untilTimestamp - new Date().getTime()), shortFormat, onlyFirstDigit) ?? 0;
return this.translate.instant('common.time-left', { time });
}
}

79
ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts

@ -16,6 +16,7 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models';
@Pipe({ @Pipe({
name: 'milliSecondsToTimeString' name: 'milliSecondsToTimeString'
@ -25,52 +26,44 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
constructor(private translate: TranslateService) { constructor(private translate: TranslateService) {
} }
transform(millseconds: number, shortFormat = false): string { transform(milliSeconds: number, shortFormat = false, onlyFirstDigit = false): string {
let seconds = Math.floor(millseconds / 1000); const { days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds);
const days = Math.floor(seconds / 86400); return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit);
let hours = Math.floor((seconds % 86400) / 3600); }
let minutes = Math.floor(((seconds % 86400) % 3600) / 60);
seconds = seconds % 60; private extractTimeUnits(milliseconds: number): { days: number; hours: number; minutes: number; seconds: number } {
const days = Math.floor(milliseconds / DAY);
const hours = Math.floor((milliseconds % DAY) / HOUR);
const minutes = Math.floor((milliseconds % HOUR) / MINUTE);
const seconds = Math.floor((milliseconds % MINUTE) / SECOND);
return { days, hours, minutes, seconds };
}
private formatTimeString(
days: number,
hours: number,
minutes: number,
seconds: number,
shortFormat: boolean,
onlyFirstDigit: boolean
): string {
const timeUnits = [
{ value: days, key: 'days', shortKey: 'short.days' },
{ value: hours, key: 'hours', shortKey: 'short.hours' },
{ value: minutes, key: 'minutes', shortKey: 'short.minutes' },
{ value: seconds, key: 'seconds', shortKey: 'short.seconds' }
];
let timeString = ''; let timeString = '';
if (shortFormat) { for (const { value, key, shortKey } of timeUnits) {
if (days > 0) { if (value > 0) {
timeString += this.translate.instant('timewindow.short.days', {days}); timeString += this.translate.instant(shortFormat ? `timewindow.${shortKey}` : `timewindow.${key}`, { [key]: value });
} if (onlyFirstDigit) {
if (hours > 0) { return timeString;
timeString += this.translate.instant('timewindow.short.hours', {hours});
}
if (minutes > 0) {
timeString += this.translate.instant('timewindow.short.minutes', {minutes});
}
if (seconds > 0) {
timeString += this.translate.instant('timewindow.short.seconds', {seconds});
}
if (!timeString.length) {
timeString += this.translate.instant('timewindow.short.seconds', {seconds: 0});
}
} else {
if (days > 0) {
timeString += this.translate.instant('timewindow.days', {days});
}
if (hours > 0) {
if (timeString.length === 0 && hours === 1) {
hours = 0;
}
timeString += this.translate.instant('timewindow.hours', {hours});
}
if (minutes > 0) {
if (timeString.length === 0 && minutes === 1) {
minutes = 0;
}
timeString += this.translate.instant('timewindow.minutes', {minutes});
}
if (seconds > 0) {
if (timeString.length === 0 && seconds === 1) {
seconds = 0;
} }
timeString += this.translate.instant('timewindow.seconds', {seconds});
} }
} }
return timeString;
return timeString.length > 0 ? timeString : this.translate.instant('timewindow.short.seconds', { seconds: 0 });
} }
} }

1
ui-ngx/src/app/shared/pipe/public-api.ts

@ -25,3 +25,4 @@ export * from './file-size.pipe';
export * from './selectable-columns.pipe'; export * from './selectable-columns.pipe';
export * from './image.pipe'; export * from './image.pipe';
export * from './key-value-not-empty.pipe'; export * from './key-value-not-empty.pipe';
export * from './duration-left.pipe';

1
ui-ngx/src/assets/locale/locale.constant-ar_AE.json

@ -4556,7 +4556,6 @@
"deselect-all": "إلغاء تحديد الكل", "deselect-all": "إلغاء تحديد الكل",
"rulenode-details": "تفاصيل عقدة القاعدة", "rulenode-details": "تفاصيل عقدة القاعدة",
"debug-mode": "وضع التصحيح", "debug-mode": "وضع التصحيح",
"singleton-mode": "وضع المفرد",
"configuration": "التكوين", "configuration": "التكوين",
"link": "رابط", "link": "رابط",
"link-details": "تفاصيل رابط عقدة القاعدة", "link-details": "تفاصيل رابط عقدة القاعدة",

23
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -988,6 +988,20 @@
"type-timeseries-deleted": "Telemetry deleted", "type-timeseries-deleted": "Telemetry deleted",
"type-sms-sent": "SMS sent" "type-sms-sent": "SMS sent"
}, },
"debug-config": {
"min": "{{number}} min",
"label": "Debug configuration",
"on-failure": "Failures only (24/7)",
"all-messages": "All messages ({{time}})",
"failures": "Failures",
"all": "All",
"hint": {
"main": "All node debug messages rate limited with:",
"main-limited": "All node debug messages will be rate-limited, with a maximum of {{msg}} messages allowed per {{sec}} seconds.",
"on-failure": "Save all failure debug events without time limit.",
"all-messages": "Save all debug events during time limit."
}
},
"confirm-on-exit": { "confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?", "message": "You have unsaved changes. Are you sure you want to leave this page?",
"html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?", "html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?",
@ -1018,11 +1032,13 @@
"enter-password": "Enter password", "enter-password": "Enter password",
"enter-search": "Enter search", "enter-search": "Enter search",
"created-time": "Created time", "created-time": "Created time",
"disabled": "Disabled",
"loading": "Loading...", "loading": "Loading...",
"proceed": "Proceed", "proceed": "Proceed",
"open-details-page": "Open details page", "open-details-page": "Open details page",
"not-found": "Not found", "not-found": "Not found",
"documentation": "Documentation" "documentation": "Documentation",
"time-left": "{{time}} left"
}, },
"content-type": { "content-type": {
"json": "Json", "json": "Json",
@ -4184,7 +4200,7 @@
"deselect-all": "Deselect all", "deselect-all": "Deselect all",
"rulenode-details": "Rule node details", "rulenode-details": "Rule node details",
"debug-mode": "Debug mode", "debug-mode": "Debug mode",
"singleton-mode": "Singleton mode", "singleton": "Singleton",
"configuration": "Configuration", "configuration": "Configuration",
"link": "Link", "link": "Link",
"link-details": "Rule node link details", "link-details": "Rule node link details",
@ -4395,6 +4411,7 @@
"tenant-profiles": "Tenant profiles", "tenant-profiles": "Tenant profiles",
"add": "Add tenant profile", "add": "Add tenant profile",
"add-profile": "Add profile", "add-profile": "Add profile",
"debug": "Debug",
"edit": "Edit tenant profile", "edit": "Edit tenant profile",
"tenant-profile-details": "Tenant profile details", "tenant-profile-details": "Tenant profile details",
"no-tenant-profiles-text": "No tenant profiles found", "no-tenant-profiles-text": "No tenant profiles found",
@ -4465,6 +4482,8 @@
"maximum-ota-packages-sum-data-size": "Maximum total size of OTA package files (bytes)", "maximum-ota-packages-sum-data-size": "Maximum total size of OTA package files (bytes)",
"maximum-ota-package-sum-data-size-required": "Maximum total size of OTA package files is required.", "maximum-ota-package-sum-data-size-required": "Maximum total size of OTA package files is required.",
"maximum-ota-package-sum-data-size-range": "Maximum total size of OTA package files can't be negative", "maximum-ota-package-sum-data-size-range": "Maximum total size of OTA package files can't be negative",
"maximum-debug-duration-min": "Maximum debug duration (min)",
"maximum-debug-duration-min-range": "Maximum debug duration can't be negative",
"rest-requests-for-tenant": "REST requests for tenant", "rest-requests-for-tenant": "REST requests for tenant",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages",
"transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points",

1
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -3485,7 +3485,6 @@
"deselect-all": "Deshacer selección de todos", "deselect-all": "Deshacer selección de todos",
"rulenode-details": "Detalles del nodo de reglas", "rulenode-details": "Detalles del nodo de reglas",
"debug-mode": "Modo Debug", "debug-mode": "Modo Debug",
"singleton-mode": "Módo único",
"configuration": "Configuración", "configuration": "Configuración",
"link": "Enlace", "link": "Enlace",
"link-details": "Detalles del enlace del nodo de reglas", "link-details": "Detalles del enlace del nodo de reglas",

1
ui-ngx/src/assets/locale/locale.constant-lt_LT.json

@ -4458,7 +4458,6 @@
"deselect-all": "Deselect all", "deselect-all": "Deselect all",
"rulenode-details": "Rule node details", "rulenode-details": "Rule node details",
"debug-mode": "Debug mode", "debug-mode": "Debug mode",
"singleton-mode": "Singleton mode",
"configuration": "Configuration", "configuration": "Configuration",
"link": "Link", "link": "Link",
"link-details": "Rule node link details", "link-details": "Rule node link details",

1
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -4515,7 +4515,6 @@
"deselect-all": "Alles deselecteren", "deselect-all": "Alles deselecteren",
"rulenode-details": "Details van rule nodes", "rulenode-details": "Details van rule nodes",
"debug-mode": "Foutopsporingsmodus", "debug-mode": "Foutopsporingsmodus",
"singleton-mode": "Singleton-modus",
"configuration": "Configuratie", "configuration": "Configuratie",
"link": "Verbinden", "link": "Verbinden",
"link-details": "Details van rule nodes links", "link-details": "Details van rule nodes links",

1
ui-ngx/src/assets/locale/locale.constant-pl_PL.json

@ -4474,7 +4474,6 @@
"deselect-all": "Odznacz wszystkie", "deselect-all": "Odznacz wszystkie",
"rulenode-details": "Szczegóły węzła reguły", "rulenode-details": "Szczegóły węzła reguły",
"debug-mode": "Tryb debugowania", "debug-mode": "Tryb debugowania",
"singleton-mode": "Tryb singletonowy",
"configuration": "Konfiguracja", "configuration": "Konfiguracja",
"link": "Połączenie", "link": "Połączenie",
"link-details": "Szczegóły połączenia węzła reguły", "link-details": "Szczegóły połączenia węzła reguły",

1
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -3968,7 +3968,6 @@
"deselect-all": "取消选择", "deselect-all": "取消选择",
"rulenode-details": "规则节点详情", "rulenode-details": "规则节点详情",
"debug-mode": "调试模式", "debug-mode": "调试模式",
"singleton-mode": "单例模式",
"configuration": "配置", "configuration": "配置",
"link": "链接", "link": "链接",
"link-details": "规则节点链接详情", "link-details": "规则节点链接详情",

18
ui-ngx/src/styles.scss

@ -1270,6 +1270,24 @@ pre.tb-highlight {
.no-wrap { .no-wrap {
white-space: nowrap; white-space: nowrap;
} }
.tb-rounded-btn {
border-radius: 20px;
padding: 0 16px;
&:not(.active, :disabled) {
color: rgba(0, 0, 0, 0.76);
background-color: rgba(0, 0, 0, 0.06);
}
&.active:not(:disabled) {
--mdc-outlined-button-outline-color: $primary;
}
&:disabled {
background: rgba(0, 0, 0, 0.06);
}
}
} }
/*************** /***************

Loading…
Cancel
Save