1use std::collections::{BTreeMap, HashSet};
4use std::fmt;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use serde::de::DeserializeOwned;
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11
12use super::error::StudyError;
13use super::task::{TaskContext, TaskResult, Workload};
14use crate::configuration::{ProjectPaths, ResolvedConfiguration};
15
16#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
18pub struct PhaseId(u64);
19
20impl PhaseId {
21 pub const fn new(value: u64) -> Self {
24 Self(value)
25 }
26
27 pub const fn get(self) -> u64 {
29 self.0
30 }
31}
32
33impl From<u64> for PhaseId {
34 fn from(value: u64) -> Self {
35 Self::new(value)
36 }
37}
38
39impl fmt::Display for PhaseId {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 self.0.fmt(formatter)
42 }
43}
44
45#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
47pub struct TaskId(Arc<str>);
48
49impl TaskId {
50 pub fn new(value: impl Into<String>) -> Self {
53 Self(value.into().into())
54 }
55
56 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62impl fmt::Display for TaskId {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 self.0.fmt(formatter)
65 }
66}
67
68#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
70pub struct TaskKey {
71 phase: PhaseId,
72 task: TaskId,
73}
74
75impl TaskKey {
76 pub fn new(phase: impl Into<PhaseId>, task: TaskId) -> Self {
78 Self {
79 phase: phase.into(),
80 task,
81 }
82 }
83
84 pub const fn phase_id(&self) -> PhaseId {
86 self.phase
87 }
88
89 pub fn task_id(&self) -> &TaskId {
91 &self.task
92 }
93}
94
95impl fmt::Display for TaskKey {
96 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(formatter, "{}/{}", self.phase, self.task)
98 }
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum TaskMode {
105 Progress,
107 OneShot,
109}
110
111pub struct Task {
113 key: TaskKey,
114 category: Arc<str>,
115 mode: TaskMode,
116 label: Arc<str>,
117 metadata: Arc<BTreeMap<String, Value>>,
118 workload: Option<Workload>,
119 completed: bool,
120}
121
122impl Task {
123 pub fn one_shot<W>(id: impl Into<String>, label: impl Into<String>, workload: W) -> Self
125 where
126 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
127 {
128 Self::new(
129 id,
130 label,
131 TaskMode::OneShot,
132 Some(Box::new(workload)),
133 false,
134 )
135 }
136
137 pub fn progress<W>(id: impl Into<String>, label: impl Into<String>, workload: W) -> Self
139 where
140 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
141 {
142 Self::new(
143 id,
144 label,
145 TaskMode::Progress,
146 Some(Box::new(workload)),
147 false,
148 )
149 }
150
151 pub fn one_shot_for_configuration<W>(
153 category: impl Into<String>,
154 configuration: &ResolvedConfiguration,
155 workload: W,
156 ) -> Self
157 where
158 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
159 {
160 Self::for_configuration(
161 category,
162 configuration,
163 TaskMode::OneShot,
164 Box::new(workload),
165 )
166 }
167
168 pub fn progress_for_configuration<W>(
170 category: impl Into<String>,
171 configuration: &ResolvedConfiguration,
172 workload: W,
173 ) -> Self
174 where
175 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
176 {
177 Self::for_configuration(
178 category,
179 configuration,
180 TaskMode::Progress,
181 Box::new(workload),
182 )
183 }
184
185 pub fn completed(id: impl Into<String>, label: impl Into<String>) -> Self {
187 Self::new(id, label, TaskMode::OneShot, None, true)
188 }
189
190 fn new(
191 id: impl Into<String>,
192 label: impl Into<String>,
193 mode: TaskMode,
194 workload: Option<Workload>,
195 completed: bool,
196 ) -> Self {
197 Self {
198 key: TaskKey::new(0, TaskId::new(id)),
199 category: "task".into(),
200 mode,
201 label: label.into().into(),
202 metadata: Arc::new(BTreeMap::new()),
203 workload,
204 completed,
205 }
206 }
207
208 fn for_configuration(
209 category: impl Into<String>,
210 configuration: &ResolvedConfiguration,
211 mode: TaskMode,
212 workload: Workload,
213 ) -> Self {
214 let category = category.into();
215 let ordinal = configuration.ordinal();
216 Self::new(
217 format!("{category}-{ordinal}"),
218 format!("{category} {ordinal}"),
219 mode,
220 Some(workload),
221 false,
222 )
223 .category(category)
224 .with_configuration(configuration)
225 }
226
227 #[must_use]
229 pub fn category(mut self, category: impl Into<String>) -> Self {
230 self.category = category.into().into();
231 self
232 }
233
234 #[must_use]
236 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
237 Arc::make_mut(&mut self.metadata).insert(key.into(), value.into());
238 self
239 }
240
241 #[must_use]
243 pub fn with_configuration(mut self, configuration: &ResolvedConfiguration) -> Self {
244 let metadata = Arc::make_mut(&mut self.metadata);
245 metadata.insert(
246 "configuration_ordinal".to_owned(),
247 Value::from(configuration.ordinal()),
248 );
249 metadata.insert("configuration".to_owned(), configuration.to_json_value());
250 self
251 }
252
253 #[must_use]
255 pub fn with_project_paths(mut self, paths: &ProjectPaths) -> Self {
256 Arc::make_mut(&mut self.metadata).insert("project_paths".to_owned(), paths.to_json_value());
257 self
258 }
259
260 pub fn key(&self) -> &TaskKey {
262 &self.key
263 }
264
265 pub fn id(&self) -> &TaskId {
267 self.key.task_id()
268 }
269
270 pub fn category_name(&self) -> &str {
272 &self.category
273 }
274
275 pub fn label(&self) -> &str {
277 &self.label
278 }
279
280 pub const fn mode(&self) -> TaskMode {
282 self.mode
283 }
284
285 pub fn metadata_value(&self, key: &str) -> Option<&Value> {
287 self.metadata.get(key)
288 }
289
290 pub fn require_metadata(&self, key: &str) -> Result<&Value, StudyError> {
292 self.metadata_value(key)
293 .ok_or_else(|| StudyError::UnknownTaskMetadata {
294 task: self.key.to_string(),
295 key: key.to_owned(),
296 })
297 }
298
299 pub fn decode_metadata<T>(&self, key: &str) -> Result<T, StudyError>
301 where
302 T: DeserializeOwned,
303 {
304 T::deserialize(self.require_metadata(key)?).map_err(|source| {
305 StudyError::DecodeTaskMetadata {
306 task: self.key.to_string(),
307 key: key.to_owned(),
308 source,
309 }
310 })
311 }
312
313 pub fn metadata_iter(&self) -> impl Iterator<Item = (&str, &Value)> + '_ {
315 self.metadata
316 .iter()
317 .map(|(key, value)| (key.as_str(), value))
318 }
319
320 pub(crate) fn metadata_map(&self) -> Arc<BTreeMap<String, Value>> {
321 Arc::clone(&self.metadata)
322 }
323
324 pub(crate) fn take_workload(&mut self) -> Option<Workload> {
325 self.workload.take()
326 }
327
328 pub(crate) fn has_workload(&self) -> bool {
329 self.workload.is_some()
330 }
331
332 pub(crate) const fn is_completed(&self) -> bool {
333 self.completed
334 }
335
336 fn attach_to_phase(&mut self, phase: PhaseId) {
337 self.key.phase = phase;
338 }
339}
340
341impl fmt::Debug for Task {
342 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
343 formatter
344 .debug_struct("Task")
345 .field("key", &self.key)
346 .field("category", &self.category)
347 .field("label", &self.label)
348 .field("mode", &self.mode)
349 .field("metadata", &self.metadata.len())
350 .finish_non_exhaustive()
351 }
352}
353
354pub struct Phase {
356 id: PhaseId,
357 label: Arc<str>,
358 tasks: Vec<Task>,
359 max_active_tasks: usize,
360 prepared_task_queue_capacity: usize,
361 delay_per_task: Option<Duration>,
362 task_timeout: Option<Duration>,
363 deadline_after: Option<Duration>,
364 dependencies: Vec<PhaseId>,
365 failure_policy: PhaseFailurePolicy,
366 require_confirm: bool,
367}
368
369#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
371#[non_exhaustive]
372pub enum PhaseFailurePolicy {
373 #[default]
375 FailFast,
376 FinishActive,
378}
379
380impl PhaseFailurePolicy {
381 pub const fn as_str(self) -> &'static str {
383 match self {
384 Self::FailFast => "fail-fast",
385 Self::FinishActive => "finish-active",
386 }
387 }
388}
389
390impl Phase {
391 pub fn builder(id: impl Into<PhaseId>, label: impl Into<String>) -> PhaseBuilder {
393 PhaseBuilder {
394 id: id.into(),
395 label: label.into(),
396 tasks: Vec::new(),
397 max_active_tasks: 1,
398 prepared_task_queue_capacity: 1,
399 delay_per_task: None,
400 task_timeout: None,
401 deadline_after: None,
402 dependencies: Vec::new(),
403 failure_policy: PhaseFailurePolicy::FailFast,
404 require_confirm: false,
405 }
406 }
407
408 pub const fn id(&self) -> PhaseId {
410 self.id
411 }
412
413 pub fn label(&self) -> &str {
415 &self.label
416 }
417
418 pub fn tasks(&self) -> &[Task] {
420 &self.tasks
421 }
422
423 pub const fn max_active_tasks(&self) -> usize {
425 self.max_active_tasks
426 }
427
428 pub const fn prepared_task_queue_capacity(&self) -> usize {
430 self.prepared_task_queue_capacity
431 }
432
433 pub const fn delay_per_task(&self) -> Option<Duration> {
435 self.delay_per_task
436 }
437
438 pub const fn task_timeout(&self) -> Option<Duration> {
440 self.task_timeout
441 }
442
443 pub const fn deadline_after(&self) -> Option<Duration> {
445 self.deadline_after
446 }
447
448 pub fn dependencies(&self) -> &[PhaseId] {
450 &self.dependencies
451 }
452
453 pub const fn failure_policy(&self) -> PhaseFailurePolicy {
455 self.failure_policy
456 }
457
458 pub const fn requires_confirmation(&self) -> bool {
461 self.require_confirm
462 }
463
464 pub(crate) fn into_tasks(self) -> Vec<Task> {
465 self.tasks
466 }
467
468 pub fn task(&self, id: &TaskId) -> Option<&Task> {
470 self.tasks.iter().find(|task| task.key.task_id() == id)
471 }
472
473 pub fn unique_task_matching(&self, selector: &TaskSelector) -> Result<&Task, StudyError> {
475 let mut matches = self.tasks.iter().filter(|task| selector.matches(task));
476 let first = matches.next().ok_or_else(|| StudyError::TaskNotFound {
477 selector: selector.to_string(),
478 })?;
479 if let Some(second) = matches.next() {
480 return Err(StudyError::TaskSelectorAmbiguous {
481 selector: selector.to_string(),
482 first: first.key.to_string(),
483 second: second.key.to_string(),
484 });
485 }
486 Ok(first)
487 }
488}
489
490pub struct PhaseBuilder {
492 id: PhaseId,
493 label: String,
494 tasks: Vec<Task>,
495 max_active_tasks: usize,
496 prepared_task_queue_capacity: usize,
497 delay_per_task: Option<Duration>,
498 task_timeout: Option<Duration>,
499 deadline_after: Option<Duration>,
500 dependencies: Vec<PhaseId>,
501 failure_policy: PhaseFailurePolicy,
502 require_confirm: bool,
503}
504
505impl PhaseBuilder {
506 pub fn task(mut self, task: Task) -> Self {
508 self.tasks.push(task);
509 self
510 }
511
512 pub fn tasks<I>(mut self, tasks: I) -> Self
514 where
515 I: IntoIterator<Item = Task>,
516 {
517 self.tasks.extend(tasks);
518 self
519 }
520
521 pub fn max_active_tasks(mut self, maximum: usize) -> Self {
523 self.max_active_tasks = maximum;
524 self
525 }
526
527 pub fn prepared_task_queue_capacity(mut self, capacity: usize) -> Self {
529 self.prepared_task_queue_capacity = capacity;
530 self
531 }
532
533 pub fn delay_per_task(mut self, delay: Duration) -> Self {
538 self.delay_per_task = Some(delay);
539 self
540 }
541
542 pub fn task_timeout(mut self, timeout: Duration) -> Self {
547 self.task_timeout = Some(timeout);
548 self
549 }
550
551 pub fn deadline_after(mut self, deadline: Duration) -> Self {
556 self.deadline_after = Some(deadline);
557 self
558 }
559
560 pub fn depends_on(mut self, dependency: impl Into<PhaseId>) -> Self {
562 self.dependencies.push(dependency.into());
563 self
564 }
565
566 pub fn failure_policy(mut self, policy: PhaseFailurePolicy) -> Self {
568 self.failure_policy = policy;
569 self
570 }
571
572 pub fn require_confirm(mut self, require: bool) -> Self {
577 self.require_confirm = require;
578 self
579 }
580
581 pub fn build(mut self) -> Result<Phase, StudyError> {
583 if self.label.trim().is_empty() {
584 return Err(StudyError::InvalidPhaseLabel { phase: self.id.0 });
585 }
586 if self.tasks.is_empty() {
587 return Err(StudyError::EmptyPhase { phase: self.id.0 });
588 }
589 if self.max_active_tasks == 0 {
590 return Err(StudyError::InvalidPhaseWorkloadLimit { phase: self.id.0 });
591 }
592 if self.prepared_task_queue_capacity == 0 {
593 return Err(StudyError::InvalidPhaseQueueCapacity { phase: self.id.0 });
594 }
595 for (setting, duration) in [
596 ("delay_per_task", self.delay_per_task),
597 ("task_timeout", self.task_timeout),
598 ("deadline_after", self.deadline_after),
599 ] {
600 if duration.is_some_and(|duration| {
601 duration.is_zero() || Instant::now().checked_add(duration).is_none()
602 }) {
603 return Err(StudyError::InvalidPhaseTiming {
604 phase: self.id.0,
605 setting,
606 });
607 }
608 }
609
610 let mut ids = HashSet::with_capacity(self.tasks.len());
611 for task in &mut self.tasks {
612 if task.key.task_id().as_str().trim().is_empty() {
613 return Err(StudyError::InvalidTaskId { phase: self.id.0 });
614 }
615 if task.category.trim().is_empty() {
616 return Err(StudyError::InvalidTaskCategory {
617 task: task.key.task_id().to_string(),
618 });
619 }
620 if !ids.insert(task.key.task_id().clone()) {
621 return Err(StudyError::DuplicateTaskId {
622 phase: self.id.0,
623 task: task.key.task_id().to_string(),
624 });
625 }
626 task.attach_to_phase(self.id);
627 }
628
629 let mut dependencies = HashSet::with_capacity(self.dependencies.len());
630 for dependency in &self.dependencies {
631 if !dependencies.insert(*dependency) || *dependency == self.id {
632 return Err(StudyError::PhaseDependencyCycle { phase: self.id.0 });
633 }
634 }
635
636 Ok(Phase {
637 id: self.id,
638 label: self.label.into(),
639 tasks: self.tasks,
640 max_active_tasks: self.max_active_tasks,
641 prepared_task_queue_capacity: self.prepared_task_queue_capacity,
642 delay_per_task: self.delay_per_task,
643 task_timeout: self.task_timeout,
644 deadline_after: self.deadline_after,
645 dependencies: self.dependencies,
646 failure_policy: self.failure_policy,
647 require_confirm: self.require_confirm,
648 })
649 }
650}
651
652impl fmt::Debug for Phase {
653 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
654 formatter
655 .debug_struct("Phase")
656 .field("id", &self.id)
657 .field("label", &self.label)
658 .field("tasks", &self.tasks.len())
659 .field("max_active_tasks", &self.max_active_tasks)
660 .field(
661 "prepared_task_queue_capacity",
662 &self.prepared_task_queue_capacity,
663 )
664 .field("delay_per_task", &self.delay_per_task)
665 .field("task_timeout", &self.task_timeout)
666 .field("deadline_after", &self.deadline_after)
667 .field("dependencies", &self.dependencies)
668 .field("failure_policy", &self.failure_policy)
669 .field("require_confirm", &self.require_confirm)
670 .finish()
671 }
672}
673
674impl fmt::Debug for PhaseBuilder {
675 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
676 formatter
677 .debug_struct("PhaseBuilder")
678 .field("id", &self.id)
679 .field("label", &self.label)
680 .field("tasks", &self.tasks.len())
681 .field("max_active_tasks", &self.max_active_tasks)
682 .field(
683 "prepared_task_queue_capacity",
684 &self.prepared_task_queue_capacity,
685 )
686 .field("delay_per_task", &self.delay_per_task)
687 .field("task_timeout", &self.task_timeout)
688 .field("deadline_after", &self.deadline_after)
689 .field("dependencies", &self.dependencies)
690 .field("require_confirm", &self.require_confirm)
691 .finish_non_exhaustive()
692 }
693}
694
695#[derive(Clone, Debug, Default)]
697pub struct TaskSelector {
698 phase: Option<PhaseId>,
699 category: Option<Arc<str>>,
700 metadata: Vec<(Box<str>, Value)>,
701}
702
703impl TaskSelector {
704 pub fn new() -> Self {
706 Self::default()
707 }
708
709 pub fn phase(mut self, phase: impl Into<PhaseId>) -> Self {
711 self.phase = Some(phase.into());
712 self
713 }
714
715 pub fn category(mut self, category: impl Into<String>) -> Self {
717 self.category = Some(category.into().into());
718 self
719 }
720
721 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
723 let key = key.into();
724 if let Some((_, current)) = self
725 .metadata
726 .iter_mut()
727 .find(|(candidate, _)| candidate.as_ref() == key)
728 {
729 *current = value.into();
730 } else {
731 self.metadata.push((key.into_boxed_str(), value.into()));
732 }
733 self
734 }
735
736 pub(crate) fn matches(&self, task: &Task) -> bool {
737 self.phase.is_none_or(|phase| task.key.phase == phase)
738 && self
739 .category
740 .as_deref()
741 .is_none_or(|category| task.category_name() == category)
742 && self
743 .metadata
744 .iter()
745 .all(|(key, value)| task.metadata_value(key) == Some(value))
746 }
747}
748
749impl fmt::Display for TaskSelector {
750 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
751 let mut fields = Vec::new();
752 if let Some(phase) = self.phase {
753 fields.push(format!("phase={phase}"));
754 }
755 if let Some(category) = &self.category {
756 fields.push(format!("category={category}"));
757 }
758 fields.extend(
759 self.metadata
760 .iter()
761 .map(|(key, value)| format!("{key}={}", compact_value(value))),
762 );
763 formatter.write_str(&fields.join(", "))
764 }
765}
766
767fn compact_value(value: &Value) -> String {
768 match value {
769 Value::Array(values) => format!("<array:{}:{}>", values.len(), short_hash(value)),
770 Value::Object(values) => format!("<object:{}:{}>", values.len(), short_hash(value)),
771 _ => serde_json::to_string(value)
772 .expect("serde_json::Value always serializes to valid compact JSON"),
773 }
774}
775
776fn short_hash(value: &Value) -> String {
777 let bytes = serde_json::to_vec(value)
778 .expect("serde_json::Value always serializes to valid compact JSON");
779 let digest = Sha256::digest(bytes);
780 digest[..4]
781 .iter()
782 .map(|byte| format!("{byte:02x}"))
783 .collect::<Vec<_>>()
784 .join("")
785}