use super::*;
pub(crate) const DEFAULT_CHECKPOINT: &str = ".oxo-flow/checkpoint.json";
pub(crate) const SNAPSHOT_STALE_SECS: i64 = 300;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn process_reentry(
config: &mut WorkflowConfig,
rule: &Rule,
workdir: &std::path::Path,
wildcard_values: &std::collections::HashMap<String, String>,
sched: &mut oxo_flow_core::scheduler::SchedulerState,
order: &mut Vec<String>,
order_set: &mut std::collections::HashSet<String>,
dag: &mut WorkflowDag,
checkpoint: &Arc<tokio::sync::Mutex<CheckpointState>>,
) -> anyhow::Result<()> {
let manifest_path = rule.checkpoint_manifest.as_ref().ok_or_else(|| {
anyhow::anyhow!("checkpoint rule '{}' has no checkpoint_manifest", rule.name)
})?;
let expanded =
oxo_flow_core::executor::checkpoint::expand_config_in_path(manifest_path, wildcard_values);
let full = workdir.join(&expanded);
let content = std::fs::read_to_string(&full).map_err(|e| {
anyhow::anyhow!(
"checkpoint rule '{}' produced no readable manifest at '{}': {e}",
rule.name,
full.display()
)
})?;
let (group, samples, pairs) = oxo_flow_core::reentry::parse_manifest(&content)
.map_err(|e| anyhow::anyhow!("checkpoint rule '{}': {e}", rule.name))?;
if samples.is_empty() && pairs.is_empty() {
return Ok(()); }
let new_names =
oxo_flow_core::reentry::apply_reentry(config, group.as_deref(), &samples, &pairs)?;
if new_names.is_empty() {
return Ok(()); }
let round = {
let mut ck = checkpoint.lock().await;
let round = ck.reentries.iter().map(|r| r.round).max().unwrap_or(0) + 1;
if round > oxo_flow_core::reentry::MAX_REENTRY_ROUNDS {
anyhow::bail!(
"re-entry round cap ({}) exceeded — checkpoint rule '{}' keeps discovering new values; this is a workflow bug",
oxo_flow_core::reentry::MAX_REENTRY_ROUNDS,
rule.name
);
}
ck.record_reentry(oxo_flow_core::reentry::ReentryRecord {
round,
rule: rule.name.clone(),
group,
samples,
pairs,
});
round
};
for name in &new_names {
sched.add_rule(name);
order_set.insert(name.clone());
order.push(name.clone());
}
*dag = WorkflowDag::from_rules_with_config(
&config.rules,
&config_placeholder_values(&config.config),
)
.context("failed to rebuild workflow DAG after re-entry")?;
tracing::info!(
rule = %rule.name,
round = round,
new_instances = ?new_names,
"checkpoint re-entry expanded the plan"
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn process_output_pattern_discovery(
config: &mut WorkflowConfig,
instance_name: &str,
workdir: &std::path::Path,
sched: &mut oxo_flow_core::scheduler::SchedulerState,
order: &mut Vec<String>,
order_set: &mut std::collections::HashSet<String>,
dag: &mut WorkflowDag,
checkpoint: &Arc<tokio::sync::Mutex<CheckpointState>>,
checkpoint_path: &std::path::Path,
) -> anyhow::Result<Vec<String>> {
let Some(template) = config.output_pattern_template_of(instance_name) else {
return Ok(Vec::new());
};
let Some(instance) = config.get_rule(instance_name).cloned() else {
return Ok(Vec::new());
};
let combos = if config.output_pattern_needs_discovery(&instance) {
config.discover_output_pattern_files(&instance, workdir)?
} else {
let bindings = config.instance_bindings(instance_name);
if bindings.is_empty()
|| config
.pending_output_pattern_consumers_of(&template)
.is_empty()
{
return Ok(Vec::new());
}
vec![bindings]
};
if combos.is_empty() {
eprintln!(
"{} output_pattern '{}' of rule '{}' matched no files — \
downstream consumers stay uninstantiated",
"Warning:".bold().yellow(),
instance.output_pattern.as_deref().unwrap_or_default(),
instance_name
);
tracing::warn!(
rule = instance_name,
pattern = ?instance.output_pattern,
"output_pattern discovery matched no files"
);
return Ok(Vec::new());
}
config.contribute_output_pattern_domain(&template, combos.clone());
{
let mut ck = checkpoint.lock().await;
ck.record_output_pattern_domain(&template, combos);
if let Err(save_err) = ck.save_to_file(checkpoint_path) {
tracing::warn!("Failed to save checkpoint: {save_err}");
}
}
let belongs_to = |r: &Rule| {
r.output_pattern.is_some()
&& (r.name == template
|| config.expansion_templates.get(&r.name).map(String::as_str)
== Some(template.as_str()))
};
let outstanding = config.rules.iter().any(|r| {
belongs_to(r) && sched.status(&r.name) != Some(oxo_flow_core::executor::JobStatus::Success)
});
if outstanding {
return Ok(Vec::new());
}
let new_names = config.expand_output_pattern_consumers()?;
if new_names.is_empty() {
return Ok(Vec::new());
}
for name in &new_names {
sched.add_rule(name);
order_set.insert(name.clone());
order.push(name.clone());
}
*dag = WorkflowDag::from_rules_with_config(
&config.rules,
&config_placeholder_values(&config.config),
)
.context("failed to rebuild workflow DAG after output_pattern fan-out")?;
tracing::info!(
producer = template,
new_instances = ?new_names,
"output_pattern discovery expanded the plan"
);
Ok(new_names)
}