Browse Source

Merge pull request #11861 from ShvaykaD/feature/rule-node-debug-strategies

Rule node debug strategies
pull/12138/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
5c5e8a3c98
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
  2. 21
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  3. 30
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  4. 17
      application/src/main/data/upgrade/3.8.1/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. 13
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  12. 1
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 3
      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. 974
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  18. 8
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  19. 2
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  20. 10
      application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
  21. 2
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  22. 4
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  23. 8
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  24. 8
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  25. 33
      common/data/src/main/java/org/thingsboard/server/common/data/HasDebugMode.java
  26. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  27. 4
      common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java
  28. 28
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  29. 13
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  30. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java
  31. 4
      common/edge-api/src/main/proto/edge.proto
  32. 43
      common/util/src/main/java/org/thingsboard/common/util/DebugModeUtil.java
  33. 22
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  34. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  35. 13
      dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
  36. 5
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  37. 3
      dao/src/main/resources/sql/schema-entities.sql
  38. 54
      monitoring/src/main/resources/root_rule_chain.json
  39. 9
      msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json
  40. 9
      msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json
  41. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  42. 1
      ui-ngx/src/app/core/auth/auth.reducer.ts
  43. 46
      ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html
  44. 20
      ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.scss
  45. 102
      ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts
  46. 65
      ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html
  47. 112
      ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.ts
  48. 11
      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. 16
      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. 29
      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. 24
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  55. 20
      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. 4
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  58. 8
      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

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

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

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

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

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

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

17
application/src/main/data/upgrade/3.8.1/schema_update.sql

@ -25,6 +25,23 @@ UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::
UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text
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_failures boolean DEFAULT false;
ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS debug_all_until bigint NOT NULL DEFAULT 0;
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_all_until = (extract(epoch from now()) + 3600) * 1000 WHERE debug_mode = true;
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

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.PageLink;
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.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbActorMsg;
@ -130,7 +131,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED;
* Created by ashvayka on 19.03.18.
*/
@Slf4j
class DefaultTbContext implements TbContext {
public class DefaultTbContext implements TbContext {
private final ActorSystemContext mainCtx;
private final String ruleChainName;
@ -144,25 +145,20 @@ class DefaultTbContext implements TbContext {
@Override
public void tellSuccess(TbMsg msg) {
tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS), null);
tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS));
}
@Override
public void tellNext(TbMsg msg, String relationType) {
tellNext(msg, Collections.singleton(relationType), null);
tellNext(msg, Collections.singleton(relationType));
}
@Override
public void tellNext(TbMsg msg, Set<String> relationTypes) {
tellNext(msg, relationTypes, null);
}
private void tellNext(TbMsg msg, Set<String> relationTypes, Throwable th) {
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));
RuleNode ruleNode = nodeCtx.getSelf();
persistDebugOutput(msg, relationTypes);
msg.getCallback().onProcessingEnd(ruleNode.getId());
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(), ruleNode.getId(), relationTypes, msg, null));
}
@Override
@ -183,9 +179,7 @@ class DefaultTbContext implements TbContext {
if (item == null) {
ack(msg);
} else {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType);
}
persistDebugOutput(msg, relationType);
nodeCtx.getChainActor().tell(new RuleChainOutputMsg(item.getRuleChainId(), item.getRuleNodeId(), relationType, msg));
}
}
@ -214,11 +208,9 @@ class DefaultTbContext implements TbContext {
.setTenantIdMSB(getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(getTenantId().getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg)).build();
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain");
}
mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(
metadata -> {
persistDebugOutput(tbMsg, TbNodeConnectionType.TO_ROOT_RULE_CHAIN);
if (onSuccess != null) {
onSuccess.run();
}
@ -300,8 +292,9 @@ class DefaultTbContext implements TbContext {
}
return;
}
RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId();
RuleNodeId ruleNodeId = nodeCtx.getSelf().getId();
RuleNode ruleNode = nodeCtx.getSelf();
RuleChainId ruleChainId = ruleNode.getRuleChainId();
RuleNodeId ruleNodeId = ruleNode.getId();
TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId);
TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(getTenantId().getId().getMostSignificantBits())
@ -311,12 +304,9 @@ class DefaultTbContext implements TbContext {
if (failureMessage != null) {
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(
metadata -> {
persistDebugOutput(tbMsg, relationTypes, null, failureMessage);
if (onSuccess != null) {
onSuccess.run();
}
@ -332,10 +322,9 @@ class DefaultTbContext implements TbContext {
@Override
public void ack(TbMsg tbMsg) {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "ACK", null);
}
tbMsg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId());
RuleNode ruleNode = nodeCtx.getSelf();
persistDebugOutput(tbMsg, TbNodeConnectionType.ACK);
tbMsg.getCallback().onProcessingEnd(ruleNode.getId());
tbMsg.getCallback().onSuccess();
}
@ -350,12 +339,11 @@ class DefaultTbContext implements TbContext {
@Override
public void tellFailure(TbMsg msg, Throwable th) {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th);
}
RuleNode ruleNode = nodeCtx.getSelf();
persistDebugOutput(msg, Set.of(TbNodeConnectionType.FAILURE), th, null);
String failureMessage = getFailureMessage(th);
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(),
nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE),
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(),
ruleNode.getId(), Collections.singleton(TbNodeConnectionType.FAILURE),
msg, failureMessage));
}
@ -1010,4 +998,21 @@ class DefaultTbContext implements TbContext {
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.RuleChainType;
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.plugin.ComponentLifecycleMsg;
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.RuleNodeException;
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.gen.transport.TransportProtos.ToRuleEngineMsg;
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 RuleChainService service;
private final TbClusterService clusterService;
private final TbApiUsageReportClient apiUsageClient;
private String ruleChainName;
private RuleNodeId firstId;
@ -81,7 +80,6 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext, TbActorRef parent, TbActorRef self) {
super(systemContext, tenantId, ruleChain.getId());
this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChain.getName();
this.parent = parent;
this.self = self;
@ -138,7 +136,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
}
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 -> {
log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId);
RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
@ -177,7 +175,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
for (RuleNode ruleNode : ruleNodeList) {
List<EntityRelation> relations = service.getRuleNodeRelations(TenantId.SYS_TENANT_ID, ruleNode.getId());
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());
} else {
for (EntityRelation relation : relations) {
@ -238,45 +236,47 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
}
public void onRuleChainInputMsg(RuleChainInputMsg envelope) {
var msg = envelope.getMsg();
if (!checkMsgValid(msg)) {
var tbMsg = envelope.getMsg();
if (!checkMsgValid(tbMsg)) {
return;
}
if (entityId.equals(envelope.getRuleChainId())) {
onTellNext(envelope.getMsg(), false);
onTellNext(tbMsg, false);
} else {
parent.tell(envelope);
}
}
public void onRuleChainOutputMsg(RuleChainOutputMsg envelope) {
var msg = envelope.getMsg();
if (!checkMsgValid(msg)) {
var tbMsg = envelope.getMsg();
if (!checkMsgValid(tbMsg)) {
return;
}
if (entityId.equals(envelope.getRuleChainId())) {
var originatorNodeId = envelope.getTargetRuleNodeId();
RuleNodeCtx ruleNodeCtx = nodeActors.get(originatorNodeId);
if (ruleNodeCtx != null && ruleNodeCtx.getSelf().isDebugMode()) {
systemContext.persistDebugOutput(tenantId, originatorNodeId, envelope.getMsg(), envelope.getRelationType());
if (ruleNodeCtx != null) {
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 {
parent.tell(envelope);
}
}
void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) {
var msg = envelope.getMsg();
if (!checkMsgValid(msg)) {
var tbMsg = envelope.getMsg();
if (!checkMsgValid(tbMsg)) {
return;
}
try {
checkComponentStateActive(envelope.getMsg());
checkComponentStateActive(tbMsg);
if (firstNode != null) {
pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType());
pushMsgToNode(firstNode, tbMsg, envelope.getFromRelationType());
} else {
envelope.getMsg().getCallback().onSuccess();
tbMsg.getCallback().onSuccess();
}
} catch (RuleNodeException e) {
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) {
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 {
log.error("[{}][{}] RuleNodeCtx is empty", entityId, ruleChainName);
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 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);
this.ruleChainName = ruleChainName;
this.ruleChainId = ruleChainId;
@ -46,7 +47,7 @@ public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNode
@Override
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
@ -118,7 +119,6 @@ public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNode
this.ruleChainId = ruleChainId;
this.ruleChainName = ruleChainName;
this.ruleNodeId = ruleNodeId;
}
@Override

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

@ -16,11 +16,11 @@
package org.thingsboard.server.actors.ruleChain;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.TbRuleNodeUpdateException;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
@ -51,13 +51,13 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
private TbNode tbNode;
private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
, TbActorRef parent, TbActorRef self) {
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName,
RuleNodeId ruleNodeId, ActorSystemContext systemContext, TbActorCtx selfActor) {
super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName;
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));
}
@ -78,7 +78,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
if (isMyNodePartition(newRuleNode)) {
this.info = new RuleNodeInfo(entityId, ruleChainName, getName(newRuleNode));
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.defaultCtx.updateSelf(newRuleNode);
if (restartRequired) {
@ -125,12 +126,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
var tenantProfileConfiguration = getTenantProfileConfiguration();
int maxRuleNodeExecutionsPerMessage = tenantProfileConfiguration.getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
}
persistDebugInputIfAllowed(msg.getMsg(), "Self");
try {
tbNode.onMsg(defaultCtx, msg.getMsg());
} catch (Exception e) {
@ -149,12 +149,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
var tenantProfileConfiguration = getTenantProfileConfiguration();
int maxRuleNodeExecutionsPerMessage = tenantProfileConfiguration.getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
persistDebugInputIfAllowed(msg.getMsg(), msg.getFromRelationType());
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
@ -216,4 +215,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
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;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.id.TenantId;
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.
*/
@Data
@AllArgsConstructor
final class RuleNodeCtx {
public final class RuleNodeCtx {
private final TenantId tenantId;
private final TbActorRef chainActor;
private final TbActorRef selfActor;
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.queue.RuleEngineException;
import java.io.Serial;
import java.io.Serializable;
import java.util.Set;
/**
* Created by ashvayka on 19.03.18.
*/
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString
class RuleNodeToRuleChainTellNextMsg extends TbRuleEngineActorMsg implements Serializable {
public class RuleNodeToRuleChainTellNextMsg extends TbRuleEngineActorMsg implements Serializable {
@Serial
private static final long serialVersionUID = 4577026446412871820L;
@Getter
private final RuleChainId ruleChainId;
@Getter
private final RuleNodeId originator;
@Getter
private final Set<String> relationTypes;
@Getter
private final String failureMessage;
public RuleNodeToRuleChainTellNextMsg(RuleChainId ruleChainId, RuleNodeId originator, Set<String> relationTypes, TbMsg tbMsg, String failureMessage) {

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

@ -70,6 +70,15 @@ public class SystemInfoController extends BaseController {
@Value("${ui.dashboard.max_datapoints_limit}")
private long maxDatapointsLimit;
@Value("${debug_mode.max_duration:15}")
private int maxDebugModeDurationMinutes;
@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)
private BuildProperties buildProperties;
@ -141,6 +150,10 @@ public class SystemInfoController extends BaseController {
if (!currentUser.isSystemAdmin()) {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
systemParams.setMaxDebugModeDurationMinutes(tenantProfileConfiguration.getMaxDebugModeDurationMinutes(maxDebugModeDurationMinutes));
if (ruleChainDebugPerTenantLimitsEnabled) {
systemParams.setRuleChainDebugPerTenantLimitsConfiguration(ruleChainDebugPerTenantLimitsConfiguration);
}
}
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.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" +
" \"maxDPStorageDays\": 0,\n" +
" \"maxRuleNodeExecutionsPerMessage\": 50,\n" +
" \"maxDebugModeDurationMinutes\": 15,\n" +
" \"maxEmails\": 0,\n" +
" \"maxSms\": 0,\n" +
" \"maxCreatedAlarms\": 1000,\n" +

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

@ -88,7 +88,8 @@ public abstract class BaseRuleChainMetadataConstructor implements RuleChainMetad
.setIdLSB(node.getId().getId().getLeastSignificantBits())
.setType(node.getType())
.setName(node.getName())
.setDebugMode(node.isDebugMode())
.setDebugFailures(node.isDebugFailures())
.setDebugAllUntil(node.getDebugAllUntil())
.setConfiguration(JacksonUtil.toString(node.getConfiguration()))
.setAdditionalInfo(JacksonUtil.toString(node.getAdditionalInfo()))
.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.setRuleEngineExceptionsTtlDays(0);
configuration.setMaxRuleNodeExecutionsPerMessage(0);
configuration.setMaxDebugModeDurationMinutes(0);
tenantProfileData.setConfiguration(configuration);
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);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData, Function.identity(), false);
ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity(), false);
return ruleChain;
}

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

@ -494,6 +494,14 @@ actors:
# Actors statistic persistence frequency in milliseconds
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
debug_mode:
# Maximum duration (in minutes) for debug mode.
# Min value is 1 minute.
# If set in the tenant profile and valid, it will be used.
# Otherwise, the value from this setting is used.
# If value from this setting is invalid, the default value (15 minutes) will be used.
max_duration: "${DEBUG_MODE_MAX_DURATION_MINUTES:15}"
# Cache settings parameters
cache:
# caffeine or redis(7.2 - latest compatible version)

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

@ -0,0 +1,974 @@
/**
* 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.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.setDebugFailures(true);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(true);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(true);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(false);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(true);
ruleNode.setDebugAllUntil(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.setDebugFailures(true);
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.setDebugFailures(true);
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);
ruleNode.setDebugFailures(false);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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);
ruleNode.setDebugFailures(false);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(true);
ruleNode.setDebugAllUntil(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.setDebugFailures(true);
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);
ruleNode.setDebugFailures(false);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(debugFailures);
ruleNode.setDebugAllUntil(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.setDebugFailures(debugFailures);
ruleNode.setDebugAllUntil(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.setDebugFailures(true);
ruleNode.setDebugAllUntil(0);
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);
ruleNode.setDebugFailures(false);
ruleNode.setDebugAllUntil(0);
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.setDebugFailures(false);
ruleNode.setDebugAllUntil(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.setDebugFailures(debugFailures);
ruleNode.setDebugAllUntil(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.setDebugAllUntil(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);
}
}

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

@ -103,7 +103,6 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -125,8 +124,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected EdgeImitator edgeImitator;
protected Edge edge;
private Random random = new Random();
@Autowired
protected EdgeEventService edgeEventService;
@ -166,7 +163,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
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"),
"EDGE")
.getData().get(0).getId();
@ -210,7 +208,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected void updateRootRuleChainMetadata() throws Exception {
RuleChainId rootRuleChainId = getEdgeRootRuleChainId();
RuleChainMetaData rootRuleChainMetadata = doGet("/api/ruleChain/" + rootRuleChainId.getId().toString() + "/metadata", RuleChainMetaData.class);
rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugMode(random.nextBoolean()));
rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugAll(true));
doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class);
}

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

@ -228,7 +228,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
// update metadata for root rule chain
edgeImitator.expectMessageAmount(1);
metaData.getNodes().forEach(n -> n.setDebugMode(true));
metaData.getNodes().forEach(n -> n.setDebugAll(true));
doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
Assert.assertTrue(edgeImitator.waitForMessages());
ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class);

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

@ -142,7 +142,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
ruleNode1.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(TbMsgSource.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
@ -152,7 +152,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
ruleNode2.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(TbMsgSource.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@ -248,7 +248,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
ruleNode1.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(TbMsgSource.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
@ -257,7 +257,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode12 = new RuleNode();
ruleNode12.setName("Simple Rule Node 1");
ruleNode12.setType(org.thingsboard.rule.engine.flow.TbRuleChainInputNode.class.getName());
ruleNode12.setDebugMode(true);
ruleNode12.setDebugAll(true);
TbRuleChainInputNodeConfiguration configuration12 = new TbRuleChainInputNodeConfiguration();
configuration12.setRuleChainId(secondaryRuleChain.getId().getId().toString());
ruleNode12.setConfiguration(JacksonUtil.valueToTree(configuration12));
@ -282,7 +282,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
ruleNode2.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(TbMsgSource.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));

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

@ -97,7 +97,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
ruleNode.setName("Simple Rule Node");
ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode.setDebugMode(true);
ruleNode.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
configuration.setFetchTo(TbMsgSource.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));

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

@ -496,7 +496,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
ruleNode1.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@ -505,7 +505,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
ruleNode2.setDebugAll(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));

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

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

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

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

33
common/data/src/main/java/org/thingsboard/server/common/data/HasDebugMode.java

@ -0,0 +1,33 @@
/**
* 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 io.swagger.v3.oas.annotations.media.Schema;
public interface HasDebugMode {
boolean isDebugFailures();
void setDebugFailures(boolean debugFailures);
boolean isDebugAll();
void setDebugAll(boolean debugAll);
long getDebugAllUntil();
void setDebugAllUntil(long debugAllUntil);
}

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

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

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 FAILURE = "Failure";
public static final String ACK = "ACK";
public static final String TRUE = "True";
public static final String FALSE = "False";
public static final String OTHER = "Other";
public static final String TO_ROOT_RULE_CHAIN = "To Root Rule Chain";
}

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

@ -16,12 +16,14 @@
package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugMode;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -32,7 +34,8 @@ import org.thingsboard.server.common.data.validation.NoXss;
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements HasName {
@JsonIgnoreProperties(ignoreUnknown = true)
public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements HasName, HasDebugMode {
private static final long serialVersionUID = -5656679015121235465L;
@ -45,15 +48,19 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
@Length(fieldName = "name")
@Schema(description = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading")
private String name;
@Schema(description = "Enable/disable debug. ", example = "false")
private boolean debugMode;
@Schema(description = "Debug failures. ", example = "false")
private boolean debugFailures;
@Schema(description = "Debug All. Used as a trigger for updating debugAllUntil.", example = "false")
private boolean debugAll;
@Schema(description = "Timestamp of the end time for the processing debug events.")
private long debugAllUntil;
@Schema(description = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode;
@Schema(description = "Queue name. ", example = "Main")
private String queueName;
@Schema(description = "Version of rule node configuration. ", example = "0")
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;
@JsonIgnore
private byte[] configurationBytes;
@ -73,7 +80,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
this.ruleChainId = ruleNode.getRuleChainId();
this.type = ruleNode.getType();
this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode();
this.debugFailures = ruleNode.isDebugFailures();
this.debugAll = ruleNode.isDebugAll();
this.debugAllUntil = ruleNode.getDebugAllUntil();
this.singletonMode = ruleNode.isSingletonMode();
this.setConfiguration(ruleNode.getConfiguration());
this.externalId = ruleNode.getExternalId();
@ -93,9 +102,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
}
@Schema(description = "JSON object with the Rule Node Id. " +
"Specify this field to update the Rule Node. " +
"Referencing non-existing Rule Node Id will cause error. " +
"Omit this field to create new rule node.")
"Specify this field to update the Rule Node. " +
"Referencing non-existing Rule Node Id will cause error. " +
"Omit this field to create new rule node.")
@Override
public RuleNodeId getId() {
return super.getId();
@ -107,10 +116,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
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
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
}

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

@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfileType;
import java.io.Serial;
@Schema
@AllArgsConstructor
@NoArgsConstructor
@ -31,8 +33,11 @@ import org.thingsboard.server.common.data.TenantProfileType;
@Data
public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration {
@Serial
private static final long serialVersionUID = -7134932690332578595L;
private static final int DEBUG_MODE_DEFAULT_DURATION_MINUTES = 15;
private long maxDevices;
private long maxAssets;
private long maxCustomers;
@ -91,6 +96,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxDPStorageDays;
@Schema(example = "50")
private int maxRuleNodeExecutionsPerMessage;
@Schema(example = "15")
private int maxDebugModeDurationMinutes;
@Schema(example = "0")
private long maxEmails;
@Schema(example = "true")
@ -197,4 +204,10 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
public int getMaxRuleNodeExecsPerMessage() {
return maxRuleNodeExecutionsPerMessage;
}
@Override
public int getMaxDebugModeDurationMinutes(int systemMaxDebugModeDurationMinutes) {
return maxDebugModeDurationMinutes > 0 ? maxDebugModeDurationMinutes : systemMaxDebugModeDurationMinutes > 0 ?
systemMaxDebugModeDurationMinutes : DEBUG_MODE_DEFAULT_DURATION_MINUTES;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java

@ -51,4 +51,7 @@ public interface TenantProfileConfiguration extends Serializable {
@JsonIgnore
int getMaxRuleNodeExecsPerMessage();
@JsonIgnore
int getMaxDebugModeDurationMinutes(int systemMaxDebugModeDurationMinutes);
}

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

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

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

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

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

@ -18,9 +18,11 @@ package org.thingsboard.server.dao.entity;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasDebugMode;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
@ -33,11 +35,13 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.housekeeper.CleanUpService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Slf4j
public abstract class AbstractEntityService {
@ -68,6 +72,13 @@ public abstract class AbstractEntityService {
@Lazy
protected CleanUpService cleanUpService;
@Autowired
@Lazy
private TbTenantProfileCache tbTenantProfileCache;
@Value("${debug_mode.max_duration:15}")
private int maxDebugModeDurationMinutes;
protected void createRelation(TenantId tenantId, EntityRelation relation) {
log.debug("Creating relation: {}", relation);
relationService.saveRelation(tenantId, relation);
@ -124,4 +135,15 @@ public abstract class AbstractEntityService {
}
}
}
protected void setDebugAllUntil(TenantId tenantId, HasDebugMode entity, long now) {
int debugDuration = tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(maxDebugModeDurationMinutes);
long debugUntil = now + TimeUnit.MINUTES.toMillis(debugDuration);
if (entity.isDebugAll()) {
entity.setDebugAllUntil(debugUntil);
} else if (entity.getDebugAllUntil() > debugUntil) {
throw new DataValidationException("Unable to update 'debugAllUntil' property. To reset the debug duration, please modify the 'debugAll' property instead.");
}
}
}

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

@ -398,6 +398,8 @@ public class ModelConstants {
public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message";
public static final String DEBUG_MODE = "debug_mode";
public static final String DEBUG_FAILURES = "debug_failures";
public static final String DEBUG__ALL_UNTIL = "debug_all_until";
public static final String SINGLETON_MODE = "singleton_mode";
public static final String QUEUE_NAME = "queue_name";

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

@ -58,8 +58,11 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
@Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
@Column(name = ModelConstants.DEBUG_MODE)
private boolean debugMode;
@Column(name = ModelConstants.DEBUG_FAILURES)
private boolean debugFailures;
@Column(name = ModelConstants.DEBUG__ALL_UNTIL)
private long debugAllUntil;
@Column(name = ModelConstants.SINGLETON_MODE)
private boolean singletonMode;
@ -83,7 +86,8 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
}
this.type = ruleNode.getType();
this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode();
this.debugFailures = ruleNode.isDebugFailures();
this.debugAllUntil = ruleNode.getDebugAllUntil();
this.singletonMode = ruleNode.isSingletonMode();
this.queueName = ruleNode.getQueueName();
this.configurationVersion = ruleNode.getConfigurationVersion();
@ -103,7 +107,8 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
}
ruleNode.setType(type);
ruleNode.setName(name);
ruleNode.setDebugMode(debugMode);
ruleNode.setDebugFailures(debugFailures);
ruleNode.setDebugAllUntil(debugAllUntil);
ruleNode.setSingletonMode(singletonMode);
ruleNode.setQueueName(queueName);
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();
if (nodes != null) {
long now = System.currentTimeMillis();
for (RuleNode node : toAddOrUpdate) {
node.setRuleChainId(ruleChainId);
node = ruleNodeUpdater.apply(node);
setDebugAllUntil(tenantId, node, now);
RuleChainDataValidator.validateRuleNode(node);
RuleNode savedNode = ruleNodeDao.save(tenantId, node);
relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(),
@ -261,7 +265,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
layout.remove("description");
layout.remove("ruleChainNodeId");
targetNode.setAdditionalInfo(layout);
targetNode.setDebugMode(false);
targetNode = ruleNodeDao.save(tenantId, targetNode);
EntityRelation sourceRuleChainToRuleNode = new EntityRelation();

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

@ -181,7 +181,8 @@ CREATE TABLE IF NOT EXISTS rule_node (
configuration varchar(10000000),
type varchar(255),
name varchar(255),
debug_mode boolean,
debug_failures boolean,
debug_all_until bigint NOT NULL,
singleton_mode boolean,
queue_name varchar(255),
external_id uuid

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

@ -20,7 +20,8 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -35,7 +36,8 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Attributes",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 1,
"configuration": {
@ -53,7 +55,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "Message Type Switch",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -68,7 +71,8 @@
},
"type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log RPC from Device",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -85,7 +89,8 @@
},
"type": "org.thingsboard.rule.engine.action.TbLogNode",
"name": "Log Other",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -102,7 +107,8 @@
},
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"name": "RPC Call Request",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -117,7 +123,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode",
"name": "Is Entity Group",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -134,7 +141,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode",
"name": "Post attributes or RPC request",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -152,7 +160,8 @@
},
"type": "org.thingsboard.rule.engine.transform.TbDuplicateMsgToGroupNode",
"name": "Duplicate To Group Entities",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -169,7 +178,8 @@
},
"type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"name": "Device Profile Node",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -186,7 +196,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Test JS script",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -204,7 +215,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Test TBEL script",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -222,7 +234,8 @@
},
"type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
"name": "Add arrival timestamp",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -240,7 +253,8 @@
},
"type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
"name": "Calculate additional latencies",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -258,7 +272,8 @@
},
"type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode",
"name": "To latencies asset",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -287,7 +302,8 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -303,7 +319,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode",
"name": "Has testData",
"debugMode": false,
"debugFailures": false,
"debugAll": false,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {
@ -323,7 +340,8 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries with TTL",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"singletonMode": false,
"configurationVersion": 0,
"configuration": {

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

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

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

@ -8,7 +8,8 @@
},
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"name": "msgTypeSwitch",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"configuration": {
"version": 0
}
@ -20,7 +21,8 @@
},
"type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode",
"name": "formResponse",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"configuration": {
"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 +34,8 @@
},
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode",
"name": "rpcReply",
"debugMode": true,
"debugFailures": false,
"debugAll": true,
"configuration": {
"requestIdMetaDataAttribute": "requestId"
}

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

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

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

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

46
ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html

@ -0,0 +1,46 @@
<!--
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
*ngIf="!minifyMode else minifiedButton"
class="tb-rounded-btn flex-1 w-36"
color="primary"
#matButton
[class.active]="((isDebugAllActive$ | async) || debugFailures) && !disabled"
[disabled]="disabled"
(click)="openDebugStrategyPanel($event, matButton)">
<mat-icon [color]="disabled ? 'inherit' : 'primary'">bug_report</mat-icon>
<span *ngIf="!debugFailures && !(isDebugAllActive$ | async)" translate>common.disabled</span>
<span *ngIf="(isDebugAllActive$ | async) && debugFailures" translate>debug-config.all</span>
<span *ngIf="(isDebugAllActive$ | async) && !debugFailures">
{{ !debugAll ? (debugAllUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) }}
</span>
<span *ngIf="!(isDebugAllActive$ | async) && debugFailures" translate>debug-config.failures</span>
</button>
<ng-template #minifiedButton>
<button mat-icon-button
class="mini-debug-btn"
[disabled]="disabled"
#matButton
[matTooltip]="(isDebugAllActive$ | async) ? (debugAllUntil | durationLeft) : null"
matTooltipPosition="above"
(click)="openDebugStrategyPanel($event, matButton)">
<mat-icon class="material-icons" [color]="((isDebugAllActive$ | async) || debugFailures) && !disabled ? 'primary' : 'inherit'">
bug_report
</mat-icon>
</button>
</ng-template>

20
ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.scss

@ -0,0 +1,20 @@
/**
* 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.
*/
:host {
.mini-debug-btn {
color: rgba(0, 0, 0, 0.6);
}
}

102
ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts

@ -0,0 +1,102 @@
///
/// 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 {
Component,
Input,
Renderer2,
ViewContainerRef,
DestroyRef,
ChangeDetectionStrategy,
EventEmitter,
Output
} 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 { DebugConfigPanelComponent } from './debug-config-panel.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { shareReplay, timer } from 'rxjs';
import { SECOND } from '@shared/models/time/time.models';
import { HasDebugConfig } from '@shared/models/entity.models';
import { map } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
@Component({
selector: 'tb-debug-config-button',
templateUrl: './debug-config-button.component.html',
styleUrls: ['./debug-config-button.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
DurationLeftPipe,
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugConfigButtonComponent {
@Input() debugFailures = false;
@Input() debugAll = false;
@Input() debugAllUntil = 0;
@Input() disabled = false;
@Input() minifyMode = false;
@Input() debugLimitsConfiguration: string;
@Output() onDebugConfigChanged = new EventEmitter<HasDebugConfig>();
isDebugAllActive$ = timer(0, SECOND).pipe(map(() => this.debugAllUntil > new Date().getTime() || this.debugAll), shareReplay(1));
readonly maxDebugModeDurationMinutes = getCurrentAuthState(this.store).maxDebugModeDurationMinutes;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private store: Store<AppState>,
private viewContainerRef: ViewContainerRef,
private destroyRef: DestroyRef,
) {}
openDebugStrategyPanel($event: Event, matButton: MatButton): void {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DebugConfigPanelComponent, 'bottom', true, null,
{
debugFailures: this.debugFailures,
debugAll: this.debugAll,
debugAllUntil: this.debugAllUntil,
maxDebugModeDurationMinutes: this.maxDebugModeDurationMinutes,
debugLimitsConfiguration: this.debugLimitsConfiguration
},
{},
{}, {}, true);
debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
debugStrategyPopover.tbComponentRef.instance.onConfigApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((config: HasDebugConfig) => {
this.onDebugConfigChanged.emit(config);
debugStrategyPopover.hide();
});
}
}
}

65
ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html

@ -0,0 +1,65 @@
<!--
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 flex-col gap-3 p-2 max-w-sm">
<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">
<span *ngIf="debugLimitsConfiguration else noLimitHint">
{{ 'debug-config.hint.main-limited' | translate: { msg: maxMessagesCount, sec: maxTimeFrameSec } }}
</span>
<ng-template #noLimitHint>{{ 'debug-config.hint.main' | translate }}</ng-template>
</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="flex justify-between align-center">
<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) && !debugAll ? (debugAllUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) } }}
</div>
</mat-slide-toggle>
<button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !debugAll"
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>

112
ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.ts

@ -0,0 +1,112 @@
///
/// 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,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { MINUTE, SECOND } from '@shared/models/time/time.models';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HasDebugConfig } from '@shared/models/entity.models';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@Component({
selector: 'tb-debug-config-panel',
templateUrl: './debug-config-panel.component.html',
standalone: true,
imports: [
SharedModule,
CommonModule,
DurationLeftPipe
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugConfigPanelComponent extends PageComponent implements OnInit {
@Input() popover: TbPopoverComponent<DebugConfigPanelComponent>;
@Input() debugFailures = false;
@Input() debugAll = false;
@Input() debugAllUntil = 0;
@Input() maxDebugModeDurationMinutes: number;
@Input() debugLimitsConfiguration: string;
onFailuresControl = this.fb.control(false);
debugAllControl = this.fb.control(false);
maxMessagesCount: string;
maxTimeFrameSec: string;
isDebugAllActive$ = timer(0, SECOND).pipe(
map(() => {
this.cd.markForCheck();
return this.debugAllUntil > new Date().getTime() || this.debugAll;
}),
distinctUntilChanged(),
tap(isDebugOn => this.debugAllControl.patchValue(isDebugOn, { emitEvent: false })),
shareReplay(1),
);
onConfigApplied = new EventEmitter<HasDebugConfig>();
constructor(private fb: UntypedFormBuilder, private cd: ChangeDetectorRef) {
super();
this.observeDebugAllChange();
}
ngOnInit(): void {
this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0];
this.maxTimeFrameSec = this.debugLimitsConfiguration?.split(':')[1];
this.onFailuresControl.patchValue(this.debugFailures);
}
onCancel(): void {
this.popover?.hide();
}
onApply(): void {
this.onConfigApplied.emit({
debugAll: this.debugAll,
debugFailures: this.onFailuresControl.value,
debugAllUntil: this.debugAllUntil
});
}
onReset(): void {
this.debugAll = true;
this.debugAllUntil = new Date().getTime() + this.maxDebugModeDurationMinutes * MINUTE;
this.cd.markForCheck();
}
private observeDebugAllChange(): void {
this.debugAllControl.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => {
this.debugAllUntil = value? new Date().getTime() + this.maxDebugModeDurationMinutes * MINUTE : 0;
this.debugAll = value;
this.cd.markForCheck();
});
}
}

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

@ -40,8 +40,8 @@ import {
EventContentDialogData
} from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils';
import { historyInterval, MINUTE } from '@shared/models/time/time.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { DAY, historyInterval, MINUTE } from '@shared/models/time/time.models';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import {
@ -51,6 +51,9 @@ import {
FilterEntityColumn
} from '@home/components/event/event-filter-panel.component';
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> {
@ -59,6 +62,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private filterParams: FilterEventBody = {};
private filterColumns: FilterEntityColumn[] = [];
private readonly maxDebugModeDurationMinutes = getCurrentAuthState(this.store).maxDebugModeDurationMinutes;
set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) {
@ -88,13 +92,14 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private store: Store<AppState>,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
super();
this.loadDataOnInit = false;
this.tableTitle = '';
this.useTimePageLink = true;
this.defaultTimewindowInterval = historyInterval(MINUTE * 15);
this.defaultTimewindowInterval = this.maxDebugModeDurationMinutes ? historyInterval(this.maxDebugModeDurationMinutes * MINUTE) : historyInterval(DAY);
this.detailsPanelEnabled = false;
this.selectionEnabled = 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 { Subscription } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-event-table',
@ -123,6 +125,7 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
private dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private store: Store<AppState>,
private cd: ChangeDetectorRef) {
}
@ -142,6 +145,7 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.overlay,
this.viewContainerRef,
this.cd,
this.store,
this.functionTestButtonLabel,
this.debugEventSelected
);

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

@ -76,6 +76,22 @@
<mat-hint></mat-hint>
</mat-form-field>
</div>
<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 required min="0" step="1"
formControlName="maxDebugModeDurationMinutes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDebugModeDurationMinutes').hasError('required')">
{{ 'tenant-profile.maximum-debug-duration-min-range' | translate }}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDebugModeDurationMinutes').hasError('min')">
{{ 'tenant-profile.maximum-debug-duration-min-required' | translate }}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div class="flex-1"></div>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-end" translate>

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

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

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

@ -22,7 +22,7 @@
</button>
</div>
<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-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
@ -34,13 +34,26 @@
{{ 'rulenode.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<section class="node-setting">
<mat-slide-toggle formControlName="debugMode">
{{ 'rulenode.debug-mode' | translate }}
</mat-slide-toggle>
<mat-slide-toggle *ngIf="isSingleton()" formControlName="singletonMode">
{{ 'rulenode.singleton-mode' | translate }}
</mat-slide-toggle>
<section class="flex flex-row max-w-xs mb-5">
<tb-debug-config-button
class="mr-2"
[debugAll]="ruleNode.debugAll"
[debugFailures]="ruleNode.debugFailures"
[debugAllUntil]="ruleNode.debugAllUntil"
[debugLimitsConfiguration]="ruleChainDebugPerTenantLimitsConfiguration"
(onDebugConfigChanged)="onDebugConfigChanged($event)"
/>
<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>
<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;
flex-direction: column;
.node-setting {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 22px;
}
@media #{$mat-gt-sm} {
flex-direction: row;
gap: 8px;

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

@ -39,6 +39,8 @@ import { ComponentClusteringMode } from '@shared/models/component-descriptor.mod
import { coerceBoolean } from '@shared/decorators/coercion';
import { ServiceType } from '@shared/models/queue.models';
import { takeUntil } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { HasDebugConfig } from '@shared/models/entity.models';
@Component({
selector: 'tb-rule-node',
@ -79,6 +81,8 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
ruleNodeFormGroup: UntypedFormGroup;
readonly ruleChainDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
@ -92,7 +96,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
if (this.ruleNode) {
this.ruleNodeFormGroup = this.fb.group({
name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]],
debugMode: [this.ruleNode.debugMode, []],
debugAll: [this.ruleNode.debugAll],
debugFailures: [this.ruleNode.debugFailures],
debugAllUntil: [this.ruleNode.debugAllUntil],
singletonMode: [this.ruleNode.singletonMode, []],
configuration: [this.ruleNode.configuration, [Validators.required]],
additionalInfo: this.fb.group(
@ -167,6 +173,15 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
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) {
if ($event) {
$event.stopPropagation();
@ -193,4 +208,11 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
isSingletonEditAllowed() {
return this.ruleNode.component.clusteringMode === ComponentClusteringMode.USER_PREFERENCE;
}
onDebugConfigChanged(config: HasDebugConfig): void {
this.ruleNodeFormGroup.get('debugAllUntil').setValue(config.debugAllUntil);
this.ruleNodeFormGroup.get('debugAll').setValue(config.debugAll);
this.ruleNodeFormGroup.get('debugFailures').setValue(config.debugFailures);
this.ruleNodeFormGroup.markAsDirty();
}
}

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

@ -575,7 +575,9 @@ export class RuleChainPageComponent extends PageComponent
additionalInfo: ruleNode.additionalInfo,
configuration: ruleNode.configuration,
configurationVersion: isDefinedAndNotNull(ruleNode.configurationVersion) ? ruleNode.configurationVersion : 0,
debugMode: ruleNode.debugMode,
debugAll: ruleNode.debugAll,
debugFailures: ruleNode.debugFailures,
debugAllUntil: ruleNode.debugAllUntil,
singletonMode: ruleNode.singletonMode,
queueName: ruleNode.queueName,
x: Math.round(ruleNode.additionalInfo.layoutX),
@ -936,7 +938,9 @@ export class RuleChainPageComponent extends PageComponent
name: node.name,
configuration: deepClone(node.configuration),
additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {},
debugMode: node.debugMode,
debugFailures: node.debugFailures,
debugAllUntil: node.debugAllUntil,
debugAll: node.debugAll,
singletonMode: node.singletonMode,
queueName: node.queueName
};
@ -1009,7 +1013,9 @@ export class RuleChainPageComponent extends PageComponent
name: outputEdge.label,
configuration: {},
additionalInfo: {},
debugMode: false,
debugFailures: false,
debugAllUntil: 0,
debugAll: false,
singletonMode: false
};
outputNode.additionalInfo.layoutX = Math.round(destNode.x);
@ -1055,7 +1061,9 @@ export class RuleChainPageComponent extends PageComponent
configuration: {
ruleChainId: ruleChain.id.id
},
debugMode: false,
debugFailures: false,
debugAllUntil: 0,
debugAll: false,
singletonMode: false,
x: Math.round(ruleChainNodeX),
y: Math.round(ruleChainNodeY),
@ -1475,7 +1483,9 @@ export class RuleChainPageComponent extends PageComponent
: node.component.configurationVersion,
configuration: node.configuration,
additionalInfo: node.additionalInfo ? node.additionalInfo : {},
debugMode: node.debugMode,
debugFailures: node.debugFailures,
debugAllUntil: node.debugAllUntil,
debugAll: node.debugAll,
singletonMode: node.singletonMode,
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 { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component';
import { RuleNodeConfigComponent } from './rule-node-config.component';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { DebugConfigButtonComponent } from '@home/components/debug-config/debug-config-button.component';
@NgModule({
declarations: [
@ -59,7 +61,9 @@ import { RuleNodeConfigComponent } from './rule-node-config.component';
CommonModule,
SharedModule,
HomeComponentsModule,
RuleChainRoutingModule
RuleChainRoutingModule,
DurationLeftPipe,
DebugConfigButtonComponent
]
})
export class RuleChainModule { }

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

@ -658,7 +658,9 @@ export class ImportExportService {
if (ruleChainConnection.targetRuleChainId && ruleChainConnection.targetRuleChainId.id) {
const ruleChainNode: RuleNode = {
name: '',
debugMode: false,
debugFailures: false,
debugAllUntil: 0,
debugAll: false,
singletonMode: false,
type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode',
configuration: {

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

@ -193,4 +193,10 @@ export interface HasVersion {
version?: number;
}
export type VersionedEntity = EntityInfoData & HasVersion | RuleChainMetaData;
export interface HasDebugConfig {
debugAll?: boolean;
debugFailures?: boolean;
debugAllUntil?: number;
}
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 { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { TranslateService } from '@ngx-translate/core';
import { HasDebugConfig } from '@shared/models/entity.models';
export interface RuleNodeConfiguration {
[key: string]: any;
}
export interface RuleNode extends BaseData<RuleNodeId> {
export interface RuleNode extends BaseData<RuleNodeId>, HasDebugConfig {
ruleChainId?: RuleChainId;
type: string;
name: string;
debugMode: boolean;
singletonMode: boolean;
queueName?: string;
configurationVersion?: number;
@ -332,7 +331,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
configurationDescriptor?: RuleNodeConfigurationDescriptor;
}
export interface FcRuleNodeType extends FcNode {
export interface FcRuleNodeType extends FcNode, HasDebugConfig {
component?: RuleNodeComponentDescriptor;
singletonMode?: boolean;
queueName?: string;
@ -345,7 +344,6 @@ export interface FcRuleNode extends FcRuleNodeType {
ruleNodeId?: RuleNodeId;
additionalInfo?: any;
configuration?: RuleNodeConfiguration;
debugMode?: boolean;
error?: string;
highlighted?: boolean;
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 { TranslateService } from '@ngx-translate/core';
import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models';
@Pipe({
name: 'milliSecondsToTimeString'
@ -25,52 +26,44 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
constructor(private translate: TranslateService) {
}
transform(millseconds: number, shortFormat = false): string {
let seconds = Math.floor(millseconds / 1000);
const days = Math.floor(seconds / 86400);
let hours = Math.floor((seconds % 86400) / 3600);
let minutes = Math.floor(((seconds % 86400) % 3600) / 60);
seconds = seconds % 60;
transform(milliSeconds: number, shortFormat = false, onlyFirstDigit = false): string {
const { days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds);
return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit);
}
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 = '';
if (shortFormat) {
if (days > 0) {
timeString += this.translate.instant('timewindow.short.days', {days});
}
if (hours > 0) {
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;
for (const { value, key, shortKey } of timeUnits) {
if (value > 0) {
timeString += this.translate.instant(shortFormat ? `timewindow.${shortKey}` : `timewindow.${key}`, { [key]: value });
if (onlyFirstDigit) {
return timeString;
}
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 './image.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": "إلغاء تحديد الكل",
"rulenode-details": "تفاصيل عقدة القاعدة",
"debug-mode": "وضع التصحيح",
"singleton-mode": "وضع المفرد",
"configuration": "التكوين",
"link": "رابط",
"link-details": "تفاصيل رابط عقدة القاعدة",

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

@ -988,6 +988,20 @@
"type-timeseries-deleted": "Telemetry deleted",
"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": {
"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?",
@ -1018,11 +1032,13 @@
"enter-password": "Enter password",
"enter-search": "Enter search",
"created-time": "Created time",
"disabled": "Disabled",
"loading": "Loading...",
"proceed": "Proceed",
"open-details-page": "Open details page",
"not-found": "Not found",
"documentation": "Documentation"
"documentation": "Documentation",
"time-left": "{{time}} left"
},
"content-type": {
"json": "Json",
@ -4175,7 +4191,7 @@
"deselect-all": "Deselect all",
"rulenode-details": "Rule node details",
"debug-mode": "Debug mode",
"singleton-mode": "Singleton mode",
"singleton": "Singleton",
"configuration": "Configuration",
"link": "Link",
"link-details": "Rule node link details",
@ -4453,6 +4469,9 @@
"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-range": "Maximum total size of OTA package files can't be negative",
"maximum-debug-duration-min": "Maximum debug duration (min)",
"maximum-debug-duration-min-required": "Maximum debug duration is required.",
"maximum-debug-duration-min-range": "Maximum debug duration can't be negative",
"rest-requests-for-tenant": "REST requests for tenant",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages",
"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",
"rulenode-details": "Detalles del nodo de reglas",
"debug-mode": "Modo Debug",
"singleton-mode": "Módo único",
"configuration": "Configuración",
"link": "Enlace",
"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",
"rulenode-details": "Rule node details",
"debug-mode": "Debug mode",
"singleton-mode": "Singleton mode",
"configuration": "Configuration",
"link": "Link",
"link-details": "Rule node link details",

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

@ -4260,7 +4260,6 @@
"deselect-all": "Alles deselecteren",
"rulenode-details": "Details van rule nodes",
"debug-mode": "Foutopsporingsmodus",
"singleton-mode": "Singleton-modus",
"configuration": "Configuratie",
"link": "Verbinden",
"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",
"rulenode-details": "Szczegóły węzła reguły",
"debug-mode": "Tryb debugowania",
"singleton-mode": "Tryb singletonowy",
"configuration": "Konfiguracja",
"link": "Połączenie",
"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": "取消选择",
"rulenode-details": "规则节点详情",
"debug-mode": "调试模式",
"singleton-mode": "单例模式",
"configuration": "配置",
"link": "链接",
"link-details": "规则节点链接详情",

18
ui-ngx/src/styles.scss

@ -1270,6 +1270,24 @@ pre.tb-highlight {
.no-wrap {
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