mod auto_iteration;
mod chain;
mod closing;
mod container;
mod count_keys;
pub mod field_scope;
mod field_values;
mod flow;
mod flow_closing;
mod guard;
mod interpolate;
mod leaf_output;
pub mod loop_config;
pub mod node_outputs;
mod node_reporter;
mod primitive;
mod raw_params;
mod reserved_tokens;
pub mod resolve;
mod result;
mod routed;
mod segments;
pub mod template;
mod time_limit;
mod unfilled_items;
use std::rc::Rc;
use crate::context::ProcessContext;
use crate::errors::BntoError;
use crate::events::{NodeInfo, PipelineEvent, PipelineReporter};
use crate::pipeline::{
PipelineDefinition, PipelineFile, PipelineNode, PipelineResult, is_container_node,
};
use crate::registry::NodeRegistry;
use chain::run_node_chain;
use closing::ChainFailure;
use node_outputs::{CountKeys, NodeOutputs, NodeRecords};
use time_limit::TimeLimit;
#[cfg(test)]
use crate::processor::NodeInput;
#[cfg(test)]
use crate::progress::ProgressReporter;
type PipelineNodeRef<'a> = &'a PipelineNode;
struct PipelineContext<'a, F: Fn() -> u64 + Copy> {
registry: &'a NodeRegistry,
reporter: &'a PipelineReporter,
process_ctx: &'a dyn ProcessContext,
pipeline_total_files: usize,
now_ms: F,
live_clock: Rc<dyn Fn() -> u64>,
started_ms: u64,
time_limit: Option<TimeLimit>,
loop_item: Option<serde_json::Map<String, serde_json::Value>>,
fields: Rc<field_scope::FieldValues>,
parent_node_id: Option<String>,
node_outputs: NodeOutputs<'a>,
count_keys: &'a CountKeys,
}
impl<F: Fn() -> u64 + Copy> PipelineContext<'_, F> {
fn layer(&self) -> PipelineContext<'_, F> {
PipelineContext {
registry: self.registry,
reporter: self.reporter,
process_ctx: self.process_ctx,
pipeline_total_files: self.pipeline_total_files,
now_ms: self.now_ms,
live_clock: Rc::clone(&self.live_clock),
started_ms: self.started_ms,
time_limit: self.time_limit,
loop_item: self.loop_item.clone(),
fields: Rc::clone(&self.fields),
parent_node_id: self.parent_node_id.clone(),
node_outputs: self.node_outputs.layer(),
count_keys: self.count_keys,
}
}
}
#[derive(Clone, Copy)]
struct ChainPosition {
first_index: usize,
total_nodes: usize,
}
impl ChainPosition {
fn whole(total_nodes: usize) -> Self {
Self::starting_at(0, total_nodes)
}
fn starting_at(first_index: usize, total_nodes: usize) -> Self {
Self {
first_index,
total_nodes,
}
}
fn after(self, i: usize) -> Self {
self.offset(i + 1)
}
fn offset(self, i: usize) -> Self {
Self::starting_at(self.first_index + i, self.total_nodes)
}
}
struct NodeExecutionResult {
output_files: Vec<PipelineFile>,
files_processed: usize,
warnings: Vec<String>,
node_outputs: NodeRecords,
}
#[expect(clippy::too_many_lines)]
pub fn execute_pipeline(
definition: &PipelineDefinition,
files: Vec<PipelineFile>,
registry: &NodeRegistry,
reporter: &PipelineReporter,
process_ctx: &dyn ProcessContext,
now_ms: impl Fn() -> u64 + Copy + 'static,
) -> Result<PipelineResult, BntoError> {
field_values::check_field_values(&definition.nodes)?;
field_values::check_field_refs(&definition.nodes)?;
raw_params::check_raw_params(&definition.nodes, registry)?;
let flow = flow::root_flow(definition)?;
let count_keys = count_keys::count_keys_for(&definition.nodes, registry);
let ctx = build_root_context(
definition,
&files,
registry,
reporter,
process_ctx,
&count_keys,
now_ms,
);
emit_pipeline_started(&ctx, flow.order(), files.len());
let mode = definition.resolved_iteration();
let ran = flow::run_level(&ctx, &flow, files, 0, mode).and_then(|result| {
ctx.check_time_limit()
.map_err(|error| ChainFailure::new(last_node_id(&flow), error))?;
Ok(result)
});
match ran {
Ok(result) => Ok(finish_pipeline(&ctx, ctx.started_ms, result)),
Err(failure) => Err(fail_pipeline(&ctx, failure)),
}
}
fn last_node_id<'a>(flow: &crate::graph::Flow<'a>) -> &'a str {
flow.order().last().map_or("", |node| node.id.as_str())
}
fn build_root_context<'a, F: Fn() -> u64 + Copy + 'static>(
definition: &PipelineDefinition,
files: &[PipelineFile],
registry: &'a NodeRegistry,
reporter: &'a PipelineReporter,
process_ctx: &'a dyn ProcessContext,
count_keys: &'a CountKeys,
now_ms: F,
) -> PipelineContext<'a, F> {
let started_ms = now_ms();
let seed = template::build_node_outputs_for_input(&definition.nodes, files);
PipelineContext {
registry,
reporter,
process_ctx,
pipeline_total_files: files.len(),
now_ms,
live_clock: Rc::new(now_ms),
started_ms,
time_limit: TimeLimit::starting_at(started_ms, definition.time_limit_secs()),
loop_item: None,
fields: Rc::default(),
parent_node_id: None,
node_outputs: NodeOutputs::root(seed),
count_keys,
}
}
fn emit_pipeline_started<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
processing_nodes: &[&PipelineNode],
total_files: usize,
) {
let nodes = processing_nodes
.iter()
.map(|n| NodeInfo {
id: n.id.clone(),
node_type: n.node_type.clone(),
})
.collect();
ctx.reporter.emit(PipelineEvent::PipelineStarted {
total_nodes: processing_nodes.len(),
total_files,
nodes,
});
}
fn finish_pipeline<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
start_ms: u64,
result: NodeExecutionResult,
) -> PipelineResult {
let duration_ms = (ctx.now_ms)() - start_ms;
ctx.reporter.emit(PipelineEvent::PipelineCompleted {
duration_ms,
total_files_processed: result.files_processed,
});
result::build_pipeline_result(result, duration_ms)
}
fn fail_pipeline<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
failure: ChainFailure,
) -> BntoError {
let node_id = failure.node_id.clone();
let error = failure.into_verdict();
ctx.reporter.emit(PipelineEvent::PipelineFailed {
node_id,
error: error.to_string(),
});
error
}
fn emit_node_failure<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node_id: &str,
error: BntoError,
) -> BntoError {
ctx.reporter.emit(PipelineEvent::NodeFailed {
node_id: node_id.to_string(),
error: error.to_string(),
parent_node_id: ctx.parent_node_id.clone(),
});
error
}
fn execute_node<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node: &PipelineNode,
files: Vec<PipelineFile>,
node_index: usize,
total_nodes: usize,
file_offset: usize,
) -> Result<NodeExecutionResult, BntoError> {
ctx.check_time_limit()?;
let files = match guard::decide(ctx, node, (node_index, total_nodes), files) {
Ok(guard::Decision::Run(files)) => files,
Ok(guard::Decision::Skipped(result)) => return Ok(result),
Err(error) => return Err(emit_node_failure(ctx, &node.id, error)),
};
let node_start = (ctx.now_ms)();
emit_node_started(ctx, node, node_index, total_nodes);
let result = dispatch_node(ctx, node, files, file_offset);
match result {
Ok(exec_result) => {
emit_node_completed(ctx, &node.id, node_start, exec_result.files_processed);
Ok(exec_result)
}
Err(error) => Err(emit_node_failure(ctx, &node.id, error)),
}
}
fn emit_node_started<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node: &PipelineNode,
node_index: usize,
total_nodes: usize,
) {
ctx.reporter.emit(PipelineEvent::NodeStarted {
node_id: node.id.clone(),
node_index,
total_nodes,
node_type: node.node_type.clone(),
parent_node_id: ctx.parent_node_id.clone(),
});
}
fn emit_node_completed<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node_id: &str,
node_start: u64,
files_processed: usize,
) {
let duration_ms = (ctx.now_ms)() - node_start;
ctx.reporter.emit(PipelineEvent::NodeCompleted {
node_id: node_id.to_string(),
duration_ms,
files_processed,
parent_node_id: ctx.parent_node_id.clone(),
});
}
fn dispatch_node<F: Fn() -> u64 + Copy>(
ctx: &PipelineContext<F>,
node: &PipelineNode,
files: Vec<PipelineFile>,
file_offset: usize,
) -> Result<NodeExecutionResult, BntoError> {
if is_container_node(&node.node_type) {
container::execute_container_node(ctx, node, files, file_offset)
} else {
primitive::execute_primitive_node(ctx, node, files, file_offset)
}
}
#[cfg(test)]
mod tests;