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(
250 "configuration_identity".to_owned(),
251 serde_json::json!({
252 "phase_group": configuration.phase_group(),
253 "phase": configuration.phase(),
254 "global_ordinal": configuration.global_ordinal(),
255 "group_ordinal": configuration.group_ordinal(),
256 "phase_ordinal": configuration.phase_ordinal(),
257 }),
258 );
259 metadata.insert("configuration".to_owned(), configuration.to_json_value());
260 self
261 }
262
263 #[must_use]
265 pub fn with_project_paths(mut self, paths: &ProjectPaths) -> Self {
266 Arc::make_mut(&mut self.metadata).insert("project_paths".to_owned(), paths.to_json_value());
267 self
268 }
269
270 pub fn key(&self) -> &TaskKey {
272 &self.key
273 }
274
275 pub fn id(&self) -> &TaskId {
277 self.key.task_id()
278 }
279
280 pub fn category_name(&self) -> &str {
282 &self.category
283 }
284
285 pub fn label(&self) -> &str {
287 &self.label
288 }
289
290 pub const fn mode(&self) -> TaskMode {
292 self.mode
293 }
294
295 pub fn metadata_value(&self, key: &str) -> Option<&Value> {
297 self.metadata.get(key)
298 }
299
300 pub fn require_metadata(&self, key: &str) -> Result<&Value, StudyError> {
302 self.metadata_value(key)
303 .ok_or_else(|| StudyError::UnknownTaskMetadata {
304 task: self.key.to_string(),
305 key: key.to_owned(),
306 })
307 }
308
309 pub fn decode_metadata<T>(&self, key: &str) -> Result<T, StudyError>
311 where
312 T: DeserializeOwned,
313 {
314 T::deserialize(self.require_metadata(key)?).map_err(|source| {
315 StudyError::DecodeTaskMetadata {
316 task: self.key.to_string(),
317 key: key.to_owned(),
318 source,
319 }
320 })
321 }
322
323 pub fn metadata_iter(&self) -> impl Iterator<Item = (&str, &Value)> + '_ {
325 self.metadata
326 .iter()
327 .map(|(key, value)| (key.as_str(), value))
328 }
329
330 pub(crate) fn metadata_map(&self) -> Arc<BTreeMap<String, Value>> {
331 Arc::clone(&self.metadata)
332 }
333
334 pub(crate) fn take_workload(&mut self) -> Option<Workload> {
335 self.workload.take()
336 }
337
338 pub(crate) fn has_workload(&self) -> bool {
339 self.workload.is_some()
340 }
341
342 pub(crate) const fn is_completed(&self) -> bool {
343 self.completed
344 }
345
346 fn attach_to_phase(&mut self, phase: PhaseId) {
347 self.key.phase = phase;
348 }
349}
350
351impl fmt::Debug for Task {
352 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
353 formatter
354 .debug_struct("Task")
355 .field("key", &self.key)
356 .field("category", &self.category)
357 .field("label", &self.label)
358 .field("mode", &self.mode)
359 .field("metadata", &self.metadata.len())
360 .finish_non_exhaustive()
361 }
362}
363
364pub struct Phase {
366 id: PhaseId,
367 label: Arc<str>,
368 tasks: Vec<Task>,
369 max_active_tasks: usize,
370 prepared_task_queue_capacity: usize,
371 delay_per_task: Option<Duration>,
372 task_timeout: Option<Duration>,
373 deadline_after: Option<Duration>,
374 dependencies: Vec<PhaseId>,
375 failure_policy: PhaseFailurePolicy,
376 require_confirm: bool,
377}
378
379#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
381#[non_exhaustive]
382pub enum PhaseFailurePolicy {
383 #[default]
385 FailFast,
386 FinishActive,
388}
389
390impl PhaseFailurePolicy {
391 pub const fn as_str(self) -> &'static str {
393 match self {
394 Self::FailFast => "fail-fast",
395 Self::FinishActive => "finish-active",
396 }
397 }
398}
399
400impl Phase {
401 pub fn builder(id: impl Into<PhaseId>, label: impl Into<String>) -> PhaseBuilder {
403 PhaseBuilder {
404 id: id.into(),
405 label: label.into(),
406 tasks: Vec::new(),
407 max_active_tasks: 1,
408 prepared_task_queue_capacity: 1,
409 delay_per_task: None,
410 task_timeout: None,
411 deadline_after: None,
412 dependencies: Vec::new(),
413 failure_policy: PhaseFailurePolicy::FailFast,
414 require_confirm: false,
415 }
416 }
417
418 pub const fn id(&self) -> PhaseId {
420 self.id
421 }
422
423 pub fn label(&self) -> &str {
425 &self.label
426 }
427
428 pub fn tasks(&self) -> &[Task] {
430 &self.tasks
431 }
432
433 pub const fn max_active_tasks(&self) -> usize {
435 self.max_active_tasks
436 }
437
438 pub const fn prepared_task_queue_capacity(&self) -> usize {
440 self.prepared_task_queue_capacity
441 }
442
443 pub const fn delay_per_task(&self) -> Option<Duration> {
445 self.delay_per_task
446 }
447
448 pub const fn task_timeout(&self) -> Option<Duration> {
450 self.task_timeout
451 }
452
453 pub const fn deadline_after(&self) -> Option<Duration> {
455 self.deadline_after
456 }
457
458 pub fn dependencies(&self) -> &[PhaseId] {
460 &self.dependencies
461 }
462
463 pub const fn failure_policy(&self) -> PhaseFailurePolicy {
465 self.failure_policy
466 }
467
468 pub const fn requires_confirmation(&self) -> bool {
471 self.require_confirm
472 }
473
474 pub(crate) fn into_tasks(self) -> Vec<Task> {
475 self.tasks
476 }
477
478 pub fn task(&self, id: &TaskId) -> Option<&Task> {
480 self.tasks.iter().find(|task| task.key.task_id() == id)
481 }
482
483 pub fn unique_task_matching(&self, selector: &TaskSelector) -> Result<&Task, StudyError> {
485 let mut matches = self.tasks.iter().filter(|task| selector.matches(task));
486 let first = matches.next().ok_or_else(|| StudyError::TaskNotFound {
487 selector: selector.to_string(),
488 })?;
489 if let Some(second) = matches.next() {
490 return Err(StudyError::TaskSelectorAmbiguous {
491 selector: selector.to_string(),
492 first: first.key.to_string(),
493 second: second.key.to_string(),
494 });
495 }
496 Ok(first)
497 }
498}
499
500pub struct PhaseBuilder {
502 id: PhaseId,
503 label: String,
504 tasks: Vec<Task>,
505 max_active_tasks: usize,
506 prepared_task_queue_capacity: usize,
507 delay_per_task: Option<Duration>,
508 task_timeout: Option<Duration>,
509 deadline_after: Option<Duration>,
510 dependencies: Vec<PhaseId>,
511 failure_policy: PhaseFailurePolicy,
512 require_confirm: bool,
513}
514
515impl PhaseBuilder {
516 pub fn task(mut self, task: Task) -> Self {
518 self.tasks.push(task);
519 self
520 }
521
522 pub fn tasks<I>(mut self, tasks: I) -> Self
524 where
525 I: IntoIterator<Item = Task>,
526 {
527 self.tasks.extend(tasks);
528 self
529 }
530
531 pub fn max_active_tasks(mut self, maximum: usize) -> Self {
533 self.max_active_tasks = maximum;
534 self
535 }
536
537 pub fn prepared_task_queue_capacity(mut self, capacity: usize) -> Self {
539 self.prepared_task_queue_capacity = capacity;
540 self
541 }
542
543 pub fn delay_per_task(mut self, delay: Duration) -> Self {
548 self.delay_per_task = Some(delay);
549 self
550 }
551
552 pub fn task_timeout(mut self, timeout: Duration) -> Self {
557 self.task_timeout = Some(timeout);
558 self
559 }
560
561 pub fn deadline_after(mut self, deadline: Duration) -> Self {
566 self.deadline_after = Some(deadline);
567 self
568 }
569
570 pub fn depends_on(mut self, dependency: impl Into<PhaseId>) -> Self {
572 self.dependencies.push(dependency.into());
573 self
574 }
575
576 pub fn failure_policy(mut self, policy: PhaseFailurePolicy) -> Self {
578 self.failure_policy = policy;
579 self
580 }
581
582 pub fn require_confirm(mut self, require: bool) -> Self {
587 self.require_confirm = require;
588 self
589 }
590
591 pub fn build(mut self) -> Result<Phase, StudyError> {
593 if self.label.trim().is_empty() {
594 return Err(StudyError::InvalidPhaseLabel { phase: self.id.0 });
595 }
596 if self.tasks.is_empty() {
597 return Err(StudyError::EmptyPhase { phase: self.id.0 });
598 }
599 if self.max_active_tasks == 0 {
600 return Err(StudyError::InvalidPhaseWorkloadLimit { phase: self.id.0 });
601 }
602 if self.prepared_task_queue_capacity == 0 {
603 return Err(StudyError::InvalidPhaseQueueCapacity { phase: self.id.0 });
604 }
605 for (setting, duration) in [
606 ("delay_per_task", self.delay_per_task),
607 ("task_timeout", self.task_timeout),
608 ("deadline_after", self.deadline_after),
609 ] {
610 if duration.is_some_and(|duration| {
611 duration.is_zero() || Instant::now().checked_add(duration).is_none()
612 }) {
613 return Err(StudyError::InvalidPhaseTiming {
614 phase: self.id.0,
615 setting,
616 });
617 }
618 }
619
620 let mut ids = HashSet::with_capacity(self.tasks.len());
621 for task in &mut self.tasks {
622 if task.key.task_id().as_str().trim().is_empty() {
623 return Err(StudyError::InvalidTaskId { phase: self.id.0 });
624 }
625 if task.category.trim().is_empty() {
626 return Err(StudyError::InvalidTaskCategory {
627 task: task.key.task_id().to_string(),
628 });
629 }
630 if !ids.insert(task.key.task_id().clone()) {
631 return Err(StudyError::DuplicateTaskId {
632 phase: self.id.0,
633 task: task.key.task_id().to_string(),
634 });
635 }
636 task.attach_to_phase(self.id);
637 }
638
639 let mut dependencies = HashSet::with_capacity(self.dependencies.len());
640 for dependency in &self.dependencies {
641 if !dependencies.insert(*dependency) || *dependency == self.id {
642 return Err(StudyError::PhaseDependencyCycle { phase: self.id.0 });
643 }
644 }
645
646 Ok(Phase {
647 id: self.id,
648 label: self.label.into(),
649 tasks: self.tasks,
650 max_active_tasks: self.max_active_tasks,
651 prepared_task_queue_capacity: self.prepared_task_queue_capacity,
652 delay_per_task: self.delay_per_task,
653 task_timeout: self.task_timeout,
654 deadline_after: self.deadline_after,
655 dependencies: self.dependencies,
656 failure_policy: self.failure_policy,
657 require_confirm: self.require_confirm,
658 })
659 }
660}
661
662impl fmt::Debug for Phase {
663 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
664 formatter
665 .debug_struct("Phase")
666 .field("id", &self.id)
667 .field("label", &self.label)
668 .field("tasks", &self.tasks.len())
669 .field("max_active_tasks", &self.max_active_tasks)
670 .field(
671 "prepared_task_queue_capacity",
672 &self.prepared_task_queue_capacity,
673 )
674 .field("delay_per_task", &self.delay_per_task)
675 .field("task_timeout", &self.task_timeout)
676 .field("deadline_after", &self.deadline_after)
677 .field("dependencies", &self.dependencies)
678 .field("failure_policy", &self.failure_policy)
679 .field("require_confirm", &self.require_confirm)
680 .finish()
681 }
682}
683
684impl fmt::Debug for PhaseBuilder {
685 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
686 formatter
687 .debug_struct("PhaseBuilder")
688 .field("id", &self.id)
689 .field("label", &self.label)
690 .field("tasks", &self.tasks.len())
691 .field("max_active_tasks", &self.max_active_tasks)
692 .field(
693 "prepared_task_queue_capacity",
694 &self.prepared_task_queue_capacity,
695 )
696 .field("delay_per_task", &self.delay_per_task)
697 .field("task_timeout", &self.task_timeout)
698 .field("deadline_after", &self.deadline_after)
699 .field("dependencies", &self.dependencies)
700 .field("require_confirm", &self.require_confirm)
701 .finish_non_exhaustive()
702 }
703}
704
705#[derive(Clone, Debug, Default)]
707pub struct TaskSelector {
708 phase: Option<PhaseId>,
709 category: Option<Arc<str>>,
710 metadata: Vec<(Box<str>, Value)>,
711}
712
713impl TaskSelector {
714 pub fn new() -> Self {
716 Self::default()
717 }
718
719 pub fn phase(mut self, phase: impl Into<PhaseId>) -> Self {
721 self.phase = Some(phase.into());
722 self
723 }
724
725 pub fn category(mut self, category: impl Into<String>) -> Self {
727 self.category = Some(category.into().into());
728 self
729 }
730
731 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
733 let key = key.into();
734 if let Some((_, current)) = self
735 .metadata
736 .iter_mut()
737 .find(|(candidate, _)| candidate.as_ref() == key)
738 {
739 *current = value.into();
740 } else {
741 self.metadata.push((key.into_boxed_str(), value.into()));
742 }
743 self
744 }
745
746 pub(crate) fn matches(&self, task: &Task) -> bool {
747 self.phase.is_none_or(|phase| task.key.phase == phase)
748 && self
749 .category
750 .as_deref()
751 .is_none_or(|category| task.category_name() == category)
752 && self
753 .metadata
754 .iter()
755 .all(|(key, value)| task.metadata_value(key) == Some(value))
756 }
757}
758
759impl fmt::Display for TaskSelector {
760 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
761 let mut fields = Vec::new();
762 if let Some(phase) = self.phase {
763 fields.push(format!("phase={phase}"));
764 }
765 if let Some(category) = &self.category {
766 fields.push(format!("category={category}"));
767 }
768 fields.extend(
769 self.metadata
770 .iter()
771 .map(|(key, value)| format!("{key}={}", compact_value(value))),
772 );
773 formatter.write_str(&fields.join(", "))
774 }
775}
776
777fn compact_value(value: &Value) -> String {
778 match value {
779 Value::Array(values) => format!("<array:{}:{}>", values.len(), short_hash(value)),
780 Value::Object(values) => format!("<object:{}:{}>", values.len(), short_hash(value)),
781 _ => serde_json::to_string(value)
782 .expect("serde_json::Value always serializes to valid compact JSON"),
783 }
784}
785
786fn short_hash(value: &Value) -> String {
787 let bytes = serde_json::to_vec(value)
788 .expect("serde_json::Value always serializes to valid compact JSON");
789 let digest = Sha256::digest(bytes);
790 digest[..4]
791 .iter()
792 .map(|byte| format!("{byte:02x}"))
793 .collect::<Vec<_>>()
794 .join("")
795}