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