1use std::collections::{HashMap, HashSet};
37use std::fmt;
38use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, Ordering};
40
41mod error;
42mod execution_plan;
43mod execution_record;
44mod phase;
45mod renderer;
46mod reporting;
47mod scheduler;
48mod task;
49mod timing;
50
51pub use error::RuntimeError;
52pub use execution_plan::ExecutionPlan;
53pub use execution_record::{ExecutionRecord, PhaseExecutionRecord, TaskExecutionRecord};
54pub use phase::{
55 Phase, PhaseBuilder, PhaseFailurePolicy, PhaseId, Task, TaskDisplayKind, TaskId, TaskKey,
56 TaskSelector,
57};
58pub use reporting::{
59 ActivityTask, CancellationToken, ProgressSummary, TaskIdentity, TaskProgress, TaskStatus,
60};
61pub use task::{TaskContext, TaskResult};
62
63use renderer::RuntimeOutput;
64use reporting::RuntimeReporter;
65
66static RUNTIME_OWNED: AtomicBool = AtomicBool::new(false);
67
68type SatisfiedPhaseVerifier = Arc<dyn Fn(PhaseId) -> bool + Send + Sync + 'static>;
69
70pub struct WorkflowRuntimeBuilder {
72 phases: Vec<Phase>,
73 output: RuntimeOutput,
74 satisfied_phase: Option<SatisfiedPhaseVerifier>,
75 execution_record_path: std::path::PathBuf,
76}
77
78impl WorkflowRuntimeBuilder {
79 pub fn phase(mut self, phase: Phase) -> Self {
81 self.phases.push(phase);
82 self
83 }
84
85 pub fn phases<I>(mut self, phases: I) -> Self
87 where
88 I: IntoIterator<Item = Phase>,
89 {
90 self.phases.extend(phases);
91 self
92 }
93
94 pub fn satisfied_phase_verifier<F>(mut self, verifier: F) -> Self
96 where
97 F: Fn(PhaseId) -> bool + Send + Sync + 'static,
98 {
99 self.satisfied_phase = Some(Arc::new(verifier));
100 self
101 }
102
103 pub fn automatic(mut self) -> Self {
105 self.output = RuntimeOutput::Auto;
106 self
107 }
108
109 pub fn terminal(mut self) -> Self {
111 self.output = RuntimeOutput::Terminal;
112 self
113 }
114
115 pub fn plain(mut self) -> Self {
117 self.output = RuntimeOutput::Plain;
118 self
119 }
120
121 pub fn hidden(mut self) -> Self {
123 self.output = RuntimeOutput::Hidden;
124 self
125 }
126
127 pub fn build(self) -> Result<WorkflowRuntime, RuntimeError> {
129 validate_plan(&self.phases)?;
130 Ok(WorkflowRuntime {
131 phases: self.phases,
132 output: self.output,
133 satisfied_phase: self.satisfied_phase,
134 cancellation: CancellationToken::new(),
135 execution_record_path: self.execution_record_path,
136 })
137 }
138}
139
140impl fmt::Debug for WorkflowRuntimeBuilder {
141 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142 formatter
143 .debug_struct("WorkflowRuntimeBuilder")
144 .field("phases", &self.phases.len())
145 .field("output", &self.output)
146 .field("execution_record_path", &self.execution_record_path)
147 .field(
148 "has_satisfied_phase_verifier",
149 &self.satisfied_phase.is_some(),
150 )
151 .finish_non_exhaustive()
152 }
153}
154
155pub struct WorkflowRuntime {
157 phases: Vec<Phase>,
158 output: RuntimeOutput,
159 satisfied_phase: Option<SatisfiedPhaseVerifier>,
160 cancellation: CancellationToken,
161 execution_record_path: std::path::PathBuf,
162}
163
164impl WorkflowRuntime {
165 pub fn builder(execution_record_path: impl Into<std::path::PathBuf>) -> WorkflowRuntimeBuilder {
167 WorkflowRuntimeBuilder {
168 phases: Vec::new(),
169 output: RuntimeOutput::Auto,
170 satisfied_phase: None,
171 execution_record_path: execution_record_path.into(),
172 }
173 }
174
175 pub fn phases(&self) -> &[Phase] {
177 &self.phases
178 }
179
180 pub fn execution_plan(&self) -> ExecutionPlan {
183 ExecutionPlan::from_phases(&self.phases)
184 }
185
186 pub fn write_execution_plan_json(
190 &self,
191 path: impl AsRef<std::path::Path>,
192 ) -> Result<(), RuntimeError> {
193 self.execution_plan().write_json(path)
194 }
195
196 pub fn phase(&self, id: impl Into<PhaseId>) -> Option<&Phase> {
198 let id = id.into();
199 self.phases.iter().find(|phase| phase.id() == id)
200 }
201
202 pub fn cancellation_token(&self) -> CancellationToken {
204 self.cancellation.clone()
205 }
206
207 pub fn unique_task_matching(&self, selector: &TaskSelector) -> Result<&Task, RuntimeError> {
209 let mut matches = self
210 .phases
211 .iter()
212 .flat_map(|phase| phase.tasks())
213 .filter(|task| selector.matches(task));
214 let first = matches
215 .next()
216 .ok_or_else(|| RuntimeError::ManagedTaskNotFound {
217 selector: selector.to_string(),
218 })?;
219 if let Some(second) = matches.next() {
220 return Err(RuntimeError::ManagedTaskSelectorAmbiguous {
221 selector: selector.to_string(),
222 first: first.key().to_string(),
223 second: second.key().to_string(),
224 });
225 }
226 Ok(first)
227 }
228
229 pub fn run_phases<I, P>(self, phases: I) -> Result<RuntimeSummary, RuntimeError>
231 where
232 I: IntoIterator<Item = P>,
233 P: Into<PhaseId>,
234 {
235 self.run_phases_exact(phases)
236 }
237
238 pub fn run_phases_exact<I, P>(self, phases: I) -> Result<RuntimeSummary, RuntimeError>
240 where
241 I: IntoIterator<Item = P>,
242 P: Into<PhaseId>,
243 {
244 let selected = self.select_exact(phases)?;
245 self.execute(selected)
246 }
247
248 pub fn run_phases_with_dependencies<I, P>(
250 self,
251 phases: I,
252 ) -> Result<RuntimeSummary, RuntimeError>
253 where
254 I: IntoIterator<Item = P>,
255 P: Into<PhaseId>,
256 {
257 let selected = self.select_with_dependencies(phases)?;
258 self.execute(selected)
259 }
260
261 fn select_exact<I, P>(&self, phases: I) -> Result<Vec<usize>, RuntimeError>
262 where
263 I: IntoIterator<Item = P>,
264 P: Into<PhaseId>,
265 {
266 let requested = selected_ids(&self.phases, phases)?;
267 for phase in self
268 .phases
269 .iter()
270 .filter(|phase| requested.contains(&phase.id()))
271 {
272 for dependency in phase.dependencies() {
273 if !requested.contains(dependency) && !self.is_satisfied(*dependency) {
274 return Err(RuntimeError::UnsatisfiedPhaseDependency {
275 phase: phase.id().get(),
276 dependency: dependency.get(),
277 });
278 }
279 }
280 }
281 Ok(topological_positions(&self.phases)?
282 .into_iter()
283 .filter(|position| requested.contains(&self.phases[*position].id()))
284 .collect())
285 }
286
287 fn select_with_dependencies<I, P>(&self, phases: I) -> Result<Vec<usize>, RuntimeError>
288 where
289 I: IntoIterator<Item = P>,
290 P: Into<PhaseId>,
291 {
292 let mut selected = selected_ids(&self.phases, phases)?;
293 let positions: HashMap<_, _> = self
294 .phases
295 .iter()
296 .enumerate()
297 .map(|(position, phase)| (phase.id(), position))
298 .collect();
299 let mut pending: Vec<_> = selected.iter().copied().collect();
300 while let Some(id) = pending.pop() {
301 let phase = &self.phases[positions[&id]];
302 for dependency in phase.dependencies() {
303 if !self.is_satisfied(*dependency) && selected.insert(*dependency) {
304 pending.push(*dependency);
305 }
306 }
307 }
308 Ok(topological_positions(&self.phases)?
309 .into_iter()
310 .filter(|position| selected.contains(&self.phases[*position].id()))
311 .collect())
312 }
313
314 fn is_satisfied(&self, phase: PhaseId) -> bool {
315 self.satisfied_phase
316 .as_ref()
317 .is_some_and(|verify| verify(phase))
318 }
319
320 fn execute(self, selected: Vec<usize>) -> Result<RuntimeSummary, RuntimeError> {
321 let _lease = RuntimeLease::acquire()?;
322 let total_phases = selected.len();
323 let total_tasks = selected
324 .iter()
325 .map(|position| self.phases[*position].tasks().len())
326 .sum();
327 let execution = {
328 let selected_phases = selected
329 .iter()
330 .map(|position| &self.phases[*position])
331 .collect::<Vec<_>>();
332 execution_record::ExecutionRecorder::start(
333 self.execution_record_path.clone(),
334 &selected_phases,
335 )?
336 };
337 let mut summaries = Vec::with_capacity(total_phases);
338 let mut phases = self.phases.into_iter().map(Some).collect::<Vec<_>>();
339
340 for (selection_position, phase_position) in selected.iter().copied().enumerate() {
341 let phase = phases[phase_position]
342 .take()
343 .expect("selected phase positions are unique");
344 execution.phase_started(phase.id())?;
345 renderer::phase_start(self.output, &phase, selection_position + 1, total_phases);
346 let heading = renderer::phase_heading(&phase, selection_position + 1, total_phases);
347 let builder = RuntimeReporter::for_phase(&phase, &heading)?
348 .cancellation_token(self.cancellation.clone());
349 let reporter = match self.output {
350 RuntimeOutput::Auto => builder,
351 RuntimeOutput::Terminal => builder.terminal(),
352 RuntimeOutput::Plain => builder.plain(),
353 RuntimeOutput::Hidden => builder.hidden(),
354 }
355 .start()?;
356 let phase_id = phase.id();
357 let phase_label: Arc<str> = phase.label().into();
358 let require_confirm = phase.requires_confirmation();
359 let result = scheduler::execute_phase(phase, &reporter, &execution);
360 let task_execution = reporter.task_execution_snapshots();
361 let progress = if result.is_ok() {
362 reporter.complete(format!("phase {phase_id} completed"))?
363 } else {
364 reporter.fail(format!("phase {phase_id} failed"))?
365 };
366 let success = result.is_ok() && progress.is_success();
367 execution.phase_finished(phase_id, success, &progress, task_execution)?;
368 renderer::phase_complete(self.output, phase_id, &phase_label, success);
369 summaries.push(PhaseSummary {
370 id: phase_id,
371 label: phase_label,
372 progress,
373 });
374 if let Err(error) = result {
375 renderer::runtime_complete(self.output, summaries.len(), total_tasks, false);
376 let execution_record = execution.finish(false)?;
377 return Err(RuntimeError::PhaseExecutionFailed {
378 summary: RuntimeSummary {
379 phases: summaries.into(),
380 execution_record: Box::new(execution_record),
381 },
382 source: Box::new(error),
383 });
384 }
385 if require_confirm && selection_position + 1 < total_phases {
386 let next = phases[selected[selection_position + 1]]
387 .as_ref()
388 .expect("the next selected phase has not executed");
389 let confirmed = match renderer::confirm_transition(phase_id, next) {
390 Ok(confirmed) => confirmed,
391 Err(source) => {
392 renderer::runtime_complete(
393 self.output,
394 summaries.len(),
395 total_tasks,
396 false,
397 );
398 let execution_record = execution.finish(false)?;
399 return Err(RuntimeError::PhaseExecutionFailed {
400 summary: RuntimeSummary {
401 phases: summaries.into(),
402 execution_record: Box::new(execution_record),
403 },
404 source: Box::new(RuntimeError::PhaseConfirmationInput {
405 phase: phase_id.get(),
406 source,
407 }),
408 });
409 }
410 };
411 if !confirmed {
412 renderer::runtime_complete(self.output, summaries.len(), total_tasks, false);
413 let execution_record = execution.finish(false)?;
414 return Err(RuntimeError::PhaseExecutionFailed {
415 summary: RuntimeSummary {
416 phases: summaries.into(),
417 execution_record: Box::new(execution_record),
418 },
419 source: Box::new(RuntimeError::PhaseConfirmationEof {
420 phase: phase_id.get(),
421 }),
422 });
423 }
424 }
425 }
426
427 renderer::runtime_complete(self.output, summaries.len(), total_tasks, true);
428 let execution_record = execution.finish(true)?;
429 Ok(RuntimeSummary {
430 phases: summaries.into(),
431 execution_record: Box::new(execution_record),
432 })
433 }
434}
435
436impl fmt::Debug for WorkflowRuntime {
437 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
438 formatter
439 .debug_struct("WorkflowRuntime")
440 .field("phases", &self.phases.len())
441 .field(
442 "tasks",
443 &self.phases.iter().map(|p| p.tasks().len()).sum::<usize>(),
444 )
445 .field("output", &self.output)
446 .finish_non_exhaustive()
447 }
448}
449
450#[derive(Clone, Debug, Eq, PartialEq)]
452pub struct PhaseSummary {
453 id: PhaseId,
454 label: Arc<str>,
455 progress: ProgressSummary,
456}
457
458impl PhaseSummary {
459 pub const fn id(&self) -> PhaseId {
460 self.id
461 }
462
463 pub fn label(&self) -> &str {
464 &self.label
465 }
466
467 pub fn progress(&self) -> &ProgressSummary {
468 &self.progress
469 }
470
471 pub fn is_success(&self) -> bool {
472 self.progress.is_success()
473 }
474}
475
476#[derive(Clone, Debug, Eq, PartialEq)]
478pub struct RuntimeSummary {
479 phases: Arc<[PhaseSummary]>,
480 execution_record: Box<ExecutionRecord>,
481}
482
483impl RuntimeSummary {
484 pub fn phases(&self) -> &[PhaseSummary] {
485 &self.phases
486 }
487
488 pub fn execution_record(&self) -> &ExecutionRecord {
490 self.execution_record.as_ref()
491 }
492
493 pub fn total_tasks(&self) -> u64 {
494 self.phases.iter().map(|phase| phase.progress.total()).sum()
495 }
496
497 pub fn is_success(&self) -> bool {
498 !self.phases.is_empty() && self.phases.iter().all(PhaseSummary::is_success)
499 }
500}
501
502struct RuntimeLease;
503
504impl RuntimeLease {
505 fn acquire() -> Result<Self, RuntimeError> {
506 RUNTIME_OWNED
507 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
508 .map(|_| Self)
509 .map_err(|_| RuntimeError::TerminalAlreadyOwned)
510 }
511}
512
513impl Drop for RuntimeLease {
514 fn drop(&mut self) {
515 RUNTIME_OWNED.store(false, Ordering::Release);
516 }
517}
518
519fn selected_ids<I, P>(phases: &[Phase], requested: I) -> Result<HashSet<PhaseId>, RuntimeError>
520where
521 I: IntoIterator<Item = P>,
522 P: Into<PhaseId>,
523{
524 let known: HashSet<_> = phases.iter().map(Phase::id).collect();
525 let selected: HashSet<_> = requested.into_iter().map(Into::into).collect();
526 if selected.is_empty() {
527 return Err(RuntimeError::EmptyPhaseSet);
528 }
529 if let Some(unknown) = selected.iter().find(|id| !known.contains(id)) {
530 return Err(RuntimeError::UnknownSelectedPhase {
531 phase: unknown.get(),
532 });
533 }
534 Ok(selected)
535}
536
537fn validate_plan(phases: &[Phase]) -> Result<(), RuntimeError> {
538 if phases.is_empty() {
539 return Err(RuntimeError::EmptyPhaseSet);
540 }
541 let mut ids = HashSet::with_capacity(phases.len());
542 for phase in phases {
543 if !ids.insert(phase.id()) {
544 return Err(RuntimeError::DuplicatePhaseId {
545 phase: phase.id().get(),
546 });
547 }
548 }
549 for phase in phases {
550 for dependency in phase.dependencies() {
551 if !ids.contains(dependency) {
552 return Err(RuntimeError::UnknownPhaseDependency {
553 phase: phase.id().get(),
554 dependency: dependency.get(),
555 });
556 }
557 }
558 for task in phase.tasks() {
559 if !task.has_workload() && !task.is_reused() {
560 return Err(RuntimeError::MissingTaskWorkload {
561 task: task.key().to_string(),
562 });
563 }
564 }
565 }
566 topological_positions(phases).map(|_| ())
567}
568
569fn topological_positions(phases: &[Phase]) -> Result<Vec<usize>, RuntimeError> {
570 let positions: HashMap<_, _> = phases
571 .iter()
572 .enumerate()
573 .map(|(position, phase)| (phase.id(), position))
574 .collect();
575 let mut states = vec![0_u8; phases.len()];
576 let mut ordered = Vec::with_capacity(phases.len());
577 fn visit(
578 position: usize,
579 phases: &[Phase],
580 positions: &HashMap<PhaseId, usize>,
581 states: &mut [u8],
582 ordered: &mut Vec<usize>,
583 ) -> Result<(), RuntimeError> {
584 match states[position] {
585 2 => return Ok(()),
586 1 => {
587 return Err(RuntimeError::PhaseDependencyCycle {
588 phase: phases[position].id().get(),
589 });
590 }
591 _ => {}
592 }
593 states[position] = 1;
594 for dependency in phases[position].dependencies() {
595 visit(positions[dependency], phases, positions, states, ordered)?;
596 }
597 states[position] = 2;
598 ordered.push(position);
599 Ok(())
600 }
601 for position in 0..phases.len() {
602 visit(position, phases, &positions, &mut states, &mut ordered)?;
603 }
604 Ok(ordered)
605}