use std::rc::Rc;
use crate::errors::BntoError;
use crate::events::{PipelineEvent, ProgressOutputFile};
use crate::pipeline::{IterationMode, PipelineDefinition, PipelineFile};
use super::field_scope::FieldValues;
use super::flow::{level_flow_of, run_level};
use super::loop_config::{OnErrorStrategy, OutputPersistence, parse_loop_config};
use super::node_outputs::{NodeRecords, merge_records};
use super::{NodeExecutionResult, PipelineContext, PipelineNodeRef};
fn passthrough(files: Vec<PipelineFile>) -> NodeExecutionResult {
NodeExecutionResult {
files_processed: 0,
output_files: files,
warnings: Vec::new(),
node_outputs: NodeRecords::new(),
}
}
fn clone_children_definitions(node: PipelineNodeRef) -> Option<PipelineDefinition> {
let children = node.children.as_ref()?;
if children.is_empty() {
return None;
}
Some(PipelineDefinition {
nodes: children.clone(),
edges: node.edges.clone(),
settings: None,
requires: Vec::new(),
secrets: Vec::new(),
})
}
pub(super) fn execute_container_node<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node: PipelineNodeRef,
files: Vec<PipelineFile>,
file_offset: usize,
) -> Result<NodeExecutionResult, BntoError> {
let sub_definition = match clone_children_definitions(node) {
Some(def) => def,
None => return Ok(passthrough(files)),
};
match node.node_type.as_str() {
"loop" => execute_loop(ctx, node, &sub_definition, files, file_offset),
"group" | "parallel" => execute_group(ctx, node, &sub_definition, files, file_offset),
_ => Ok(passthrough(files)),
}
}
fn inside<'a, F: Fn() -> u64 + Copy>(
ctx: &'a PipelineContext<'a, F>,
container: PipelineNodeRef,
fields: &Rc<FieldValues>,
) -> PipelineContext<'a, F> {
PipelineContext {
fields: Rc::clone(fields),
parent_node_id: Some(container.id.clone()),
..ctx.layer()
}
}
#[expect(clippy::too_many_lines)]
fn execute_loop<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node: PipelineNodeRef,
sub_definition: &PipelineDefinition,
files: Vec<PipelineFile>,
file_offset: usize,
) -> Result<NodeExecutionResult, BntoError> {
let container_id = node.id.as_str();
let config = parse_loop_config(&node.params);
let fields = Rc::new(ctx.fields_for(node));
let total_iterations = files.len();
let mut all_output_files: Vec<PipelineFile> = Vec::new();
let mut total_processed: usize = 0;
let mut warnings: Vec<String> = Vec::new();
let mut records = NodeRecords::new();
for (i, file) in files.into_iter().enumerate() {
ctx.check_time_limit()?;
let iter_start = (ctx.now_ms)();
ctx.reporter.emit(PipelineEvent::IterationStarted {
node_id: container_id.to_string(),
iteration: i,
total_iterations,
});
let loop_ctx = build_loop_context(ctx, node, &fields, &file);
let result = execute_sub_pipeline(&loop_ctx, sub_definition, vec![file], file_offset + i);
match result {
Ok(exec_result) => {
let iter_duration = (ctx.now_ms)() - iter_start;
let files_produced = exec_result.output_files.len();
total_processed += exec_result.files_processed;
warnings.extend(exec_result.warnings);
merge_records(&mut records, exec_result.node_outputs, ctx.count_keys);
let progress_files =
build_progress_files(&config.output_persistence, &exec_result.output_files);
all_output_files.extend(exec_result.output_files);
ctx.reporter.emit(PipelineEvent::IterationCompleted {
node_id: container_id.to_string(),
iteration: i,
total_iterations,
duration_ms: iter_duration,
files_produced,
output_files: progress_files,
});
}
Err(error) => {
let iter_duration = (ctx.now_ms)() - iter_start;
let error_msg = error.to_string();
ctx.reporter.emit(PipelineEvent::IterationFailed {
node_id: container_id.to_string(),
iteration: i,
total_iterations,
error: error_msg.clone(),
});
if error.is_timeout() {
return Err(error);
}
match config.on_error {
OnErrorStrategy::FailFast => return Err(error),
OnErrorStrategy::Continue => {
warnings.push(format!(
"Iteration {}/{}: {}",
i + 1,
total_iterations,
error_msg
));
}
}
ctx.reporter.emit(PipelineEvent::IterationCompleted {
node_id: container_id.to_string(),
iteration: i,
total_iterations,
duration_ms: iter_duration,
files_produced: 0,
output_files: Vec::new(),
});
}
}
}
if all_output_files.is_empty() && !warnings.is_empty() {
return Err(BntoError::ProcessingFailed(format!(
"All {} iterations failed",
total_iterations
)));
}
Ok(NodeExecutionResult {
files_processed: total_processed,
output_files: all_output_files,
warnings,
node_outputs: records,
})
}
fn build_progress_files(
persistence: &OutputPersistence,
files: &[PipelineFile],
) -> Vec<ProgressOutputFile> {
match persistence {
OutputPersistence::Deferred => Vec::new(),
OutputPersistence::Progressive => files
.iter()
.map(|f| ProgressOutputFile {
name: f.name.clone(),
data: f.data.clone(),
mime_type: f.mime_type.clone(),
})
.collect(),
}
}
fn build_loop_context<'a, F: Fn() -> u64 + Copy>(
ctx: &'a PipelineContext<'a, F>,
container: PipelineNodeRef,
fields: &Rc<FieldValues>,
file: &PipelineFile,
) -> PipelineContext<'a, F> {
let item_data = if file.metadata.is_empty() {
None
} else {
Some(file.metadata.clone())
};
PipelineContext {
loop_item: item_data,
..inside(ctx, container, fields)
}
}
fn execute_group<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node: PipelineNodeRef,
sub_definition: &PipelineDefinition,
files: Vec<PipelineFile>,
file_offset: usize,
) -> Result<NodeExecutionResult, BntoError> {
let group_ctx = inside(ctx, node, &Rc::new(ctx.fields_for(node)));
execute_sub_pipeline(&group_ctx, sub_definition, files, file_offset)
}
fn execute_sub_pipeline<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
definition: &PipelineDefinition,
files: Vec<PipelineFile>,
file_offset: usize,
) -> Result<NodeExecutionResult, BntoError> {
let container = ctx.parent_node_id.as_deref();
let flow = level_flow_of(&definition.nodes, &definition.edges, container)?;
run_level(ctx, &flow, files, file_offset, IterationMode::Explicit)
.map_err(super::closing::ChainFailure::into_error)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::PipelineNode;
fn container(json: &str) -> PipelineNode {
serde_json::from_str(json).unwrap()
}
#[test]
fn sub_definition_carries_the_containers_edges() {
let node = container(
r#"{ "id": "g", "type": "group",
"nodes": [ { "id": "a", "type": "x" }, { "id": "b", "type": "x" } ],
"edges": [ { "id": "e1", "source": "a", "target": "b" } ] }"#,
);
let sub = clone_children_definitions(&node).expect("has children");
assert_eq!(sub.edges, node.edges);
assert_eq!(sub.nodes.len(), 2);
}
#[test]
fn sub_definition_of_an_edgeless_container_has_no_edges() {
let node =
container(r#"{ "id": "g", "type": "loop", "nodes": [ { "id": "a", "type": "x" } ] }"#);
let sub = clone_children_definitions(&node).expect("has children");
assert!(sub.edges.is_empty());
}
}