|
@@ -162,6 +162,13 @@ namespace {
|
|
|
// thread running the node, so the message is handed back the same way.
|
|
// thread running the node, so the message is handed back the same way.
|
|
|
thread_local std::string t_disabled_reference_error;
|
|
thread_local std::string t_disabled_reference_error;
|
|
|
|
|
|
|
|
|
|
+// Expressions that failed while evaluating one node's configuration. Collected
|
|
|
|
|
+// here for the same reason as the above - the evaluator is several calls below
|
|
|
|
|
+// the node execution and cannot reach the record itself - and drained onto the
|
|
|
|
|
+// node's result so a failure is visible in the execution rather than only in
|
|
|
|
|
+// the runner's log.
|
|
|
|
|
+thread_local std::vector<std::string> t_expression_warnings;
|
|
|
|
|
+
|
|
|
static bool configMatchesHash(const std::string& hashed, const nlohmann::json& config) {
|
|
static bool configMatchesHash(const std::string& hashed, const nlohmann::json& config) {
|
|
|
try {
|
|
try {
|
|
|
return nlohmann::json::parse(hashed) == config;
|
|
return nlohmann::json::parse(hashed) == config;
|
|
@@ -304,6 +311,11 @@ nlohmann::json ExecutionResult::toJson() const {
|
|
|
nr["output"] = keep_full_outputs ? result.output : truncateLargeValues(result.output);
|
|
nr["output"] = keep_full_outputs ? result.output : truncateLargeValues(result.output);
|
|
|
nr["error"] = result.error;
|
|
nr["error"] = result.error;
|
|
|
nr["retryCount"] = result.retry_count;
|
|
nr["retryCount"] = result.retry_count;
|
|
|
|
|
+ // Only when there is something to say, so every other record is
|
|
|
|
|
+ // unchanged and nothing has to learn a new field to read one.
|
|
|
|
|
+ if (!result.expression_warnings.empty()) {
|
|
|
|
|
+ nr["expressionWarnings"] = result.expression_warnings;
|
|
|
|
|
+ }
|
|
|
if (result.status == NodeStatus::Completed && !result.error.empty()) {
|
|
if (result.status == NodeStatus::Completed && !result.error.empty()) {
|
|
|
++tolerated_error_count;
|
|
++tolerated_error_count;
|
|
|
} else if (result.status == NodeStatus::Failed && status == ExecutionStatus::Completed) {
|
|
} else if (result.status == NodeStatus::Failed && status == ExecutionStatus::Completed) {
|
|
@@ -956,6 +968,8 @@ Result<ExecutionResult> WorkflowEngine::execute(const Workflow& workflow,
|
|
|
// Evaluate expressions in node config
|
|
// Evaluate expressions in node config
|
|
|
WorkflowNode evaluated_node = *node;
|
|
WorkflowNode evaluated_node = *node;
|
|
|
t_disabled_reference_error.clear();
|
|
t_disabled_reference_error.clear();
|
|
|
|
|
+ t_expression_warnings.clear();
|
|
|
|
|
+ t_expression_warnings.clear();
|
|
|
nlohmann::json defaulted_config = node->config;
|
|
nlohmann::json defaulted_config = node->config;
|
|
|
auto node_def_for_defaults = registry_.getNode(node->type);
|
|
auto node_def_for_defaults = registry_.getNode(node->type);
|
|
|
if (node_def_for_defaults) {
|
|
if (node_def_for_defaults) {
|
|
@@ -997,6 +1011,12 @@ Result<ExecutionResult> WorkflowEngine::execute(const Workflow& workflow,
|
|
|
node_result = executeNode(evaluated_node, input, result.execution_id, workflow,
|
|
node_result = executeNode(evaluated_node, input, result.execution_id, workflow,
|
|
|
node_def_for_defaults);
|
|
node_def_for_defaults);
|
|
|
}
|
|
}
|
|
|
|
|
+ // Whatever happened above, anything the evaluator could not
|
|
|
|
|
+ // work out belongs on this node's record.
|
|
|
|
|
+ if (!t_expression_warnings.empty()) {
|
|
|
|
|
+ node_result.expression_warnings = t_expression_warnings;
|
|
|
|
|
+ t_expression_warnings.clear();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
marker_outcome = applyNodeMarkers(node_result, node_id, node_id, result,
|
|
marker_outcome = applyNodeMarkers(node_result, node_id, node_id, result,
|
|
|
overlay, overlay_ignored, -1, callback);
|
|
overlay, overlay_ignored, -1, callback);
|
|
@@ -3606,6 +3626,14 @@ bool WorkflowEngine::executeLoopBody(
|
|
|
body_node_def_for_defaults);
|
|
body_node_def_for_defaults);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Same as the top-level walk: anything the evaluator could not
|
|
|
|
|
+ // work out belongs on this node's record, whichever branch above
|
|
|
|
|
+ // produced it.
|
|
|
|
|
+ if (!t_expression_warnings.empty()) {
|
|
|
|
|
+ body_result.expression_warnings = t_expression_warnings;
|
|
|
|
|
+ t_expression_warnings.clear();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Named for the loop that runs it and which pass this is - the
|
|
// Named for the loop that runs it and which pass this is - the
|
|
|
// same tag a node outside a loop never carries at all.
|
|
// same tag a node outside a loop never carries at all.
|
|
|
body_result.loop_node_id = loop_node_id;
|
|
body_result.loop_node_id = loop_node_id;
|
|
@@ -3656,7 +3684,26 @@ bool WorkflowEngine::executeLoopBody(
|
|
|
inner_done_data["data"] = body_result.output.value("data", nlohmann::json::object());
|
|
inner_done_data["data"] = body_result.output.value("data", nlohmann::json::object());
|
|
|
body_result.output["done"] = inner_done_data;
|
|
body_result.output["done"] = inner_done_data;
|
|
|
|
|
|
|
|
- if (!inner_success && !inner_ctx.continue_on_error) {
|
|
|
|
|
|
|
+ // The same rule the outer walk applies, and it was missing
|
|
|
|
|
+ // here: tolerating failures item by item is the point of
|
|
|
|
|
+ // continue_on_error, but a nested loop in which NOTHING
|
|
|
|
|
+ // succeeded is the iteration failing, not a tolerated failure.
|
|
|
|
|
+ //
|
|
|
|
|
+ // Without this a run reported completed while its only real
|
|
|
|
|
+ // work failed - one attachment, one iteration, the send failed,
|
|
|
|
|
+ // and the execution still finished green with a tolerated
|
|
|
|
|
+ // error. Exactly what the outer rule was written to stop.
|
|
|
|
|
+ const bool inner_nothing_succeeded =
|
|
|
|
|
+ inner_ctx.iterations_run > 0 &&
|
|
|
|
|
+ inner_ctx.iterations_failed == inner_ctx.iterations_run;
|
|
|
|
|
+ if (inner_nothing_succeeded && inner_ctx.continue_on_error) {
|
|
|
|
|
+ LOG_WARN("Nested loop {} tolerates failures, but all {} of its iterations "
|
|
|
|
|
+ "failed - failing the enclosing iteration rather than reporting "
|
|
|
|
|
+ "success", body_node_id, inner_ctx.iterations_run);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!inner_success &&
|
|
|
|
|
+ (!inner_ctx.continue_on_error || inner_nothing_succeeded)) {
|
|
|
inner_loop_left_early = true;
|
|
inner_loop_left_early = true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -4223,6 +4270,12 @@ nlohmann::json WorkflowEngine::evaluateJavaScriptExpression(
|
|
|
t_disabled_reference_error = result.error;
|
|
t_disabled_reference_error = result.error;
|
|
|
}
|
|
}
|
|
|
LOG_WARN("JavaScript expression evaluation failed: {} - Error: {}", expression, result.error);
|
|
LOG_WARN("JavaScript expression evaluation failed: {} - Error: {}", expression, result.error);
|
|
|
|
|
+ // Kept short: an expression can be long, and this is a pointer to the
|
|
|
|
|
+ // problem, not a transcript of it.
|
|
|
|
|
+ std::string shown = expression.size() > 160 ? expression.substr(0, 160) + "..."
|
|
|
|
|
+ : expression;
|
|
|
|
|
+ t_expression_warnings.push_back("Expression did not evaluate, so it produced nothing: " +
|
|
|
|
|
+ shown + " - " + result.error);
|
|
|
return nullptr;
|
|
return nullptr;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|