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};
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub struct PhaseId(u64);
18
19impl PhaseId {
20 pub const fn new(value: u64) -> Self {
23 Self(value)
24 }
25
26 pub const fn get(self) -> u64 {
28 self.0
29 }
30}
31
32impl From<u64> for PhaseId {
33 fn from(value: u64) -> Self {
34 Self::new(value)
35 }
36}
37
38impl fmt::Display for PhaseId {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 self.0.fmt(formatter)
41 }
42}
43
44#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct TaskId(Arc<str>);
47
48impl TaskId {
49 pub fn new(value: impl Into<String>) -> Self {
52 Self(value.into().into())
53 }
54
55 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61impl fmt::Display for TaskId {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 self.0.fmt(formatter)
64 }
65}
66
67#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct TaskKey {
70 phase: PhaseId,
71 task: TaskId,
72}
73
74impl TaskKey {
75 pub fn new(phase: impl Into<PhaseId>, task: TaskId) -> Self {
77 Self {
78 phase: phase.into(),
79 task,
80 }
81 }
82
83 pub const fn phase_id(&self) -> PhaseId {
85 self.phase
86 }
87
88 pub fn task_id(&self) -> &TaskId {
90 &self.task
91 }
92}
93
94impl fmt::Display for TaskKey {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(formatter, "{}/{}", self.phase, self.task)
97 }
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102#[non_exhaustive]
103pub enum TaskMode {
104 Progress,
106 OneShot,
108}
109
110pub struct Task {
112 key: TaskKey,
113 category: Arc<str>,
114 mode: TaskMode,
115 label: Arc<str>,
116 metadata: Arc<BTreeMap<String, Value>>,
117 workload: Option<Workload>,
118 completed: bool,
119}
120
121impl Task {
122 pub fn one_shot<W>(id: impl Into<String>, label: impl Into<String>, workload: W) -> Self
124 where
125 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
126 {
127 Self::new(
128 id,
129 label,
130 TaskMode::OneShot,
131 Some(Box::new(workload)),
132 false,
133 )
134 }
135
136 pub fn progress<W>(id: impl Into<String>, label: impl Into<String>, workload: W) -> Self
138 where
139 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
140 {
141 Self::new(
142 id,
143 label,
144 TaskMode::Progress,
145 Some(Box::new(workload)),
146 false,
147 )
148 }
149
150 pub fn completed(id: impl Into<String>, label: impl Into<String>) -> Self {
152 Self::new(id, label, TaskMode::OneShot, None, true)
153 }
154
155 fn new(
156 id: impl Into<String>,
157 label: impl Into<String>,
158 mode: TaskMode,
159 workload: Option<Workload>,
160 completed: bool,
161 ) -> Self {
162 Self {
163 key: TaskKey::new(0, TaskId::new(id)),
164 category: "task".into(),
165 mode,
166 label: label.into().into(),
167 metadata: Arc::new(BTreeMap::new()),
168 workload,
169 completed,
170 }
171 }
172
173 #[must_use]
175 pub fn category(mut self, category: impl Into<String>) -> Self {
176 self.category = category.into().into();
177 self
178 }
179
180 #[must_use]
182 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
183 Arc::make_mut(&mut self.metadata).insert(key.into(), value.into());
184 self
185 }
186
187 pub fn key(&self) -> &TaskKey {
189 &self.key
190 }
191
192 pub fn id(&self) -> &TaskId {
194 self.key.task_id()
195 }
196
197 pub fn category_name(&self) -> &str {
199 &self.category
200 }
201
202 pub fn label(&self) -> &str {
204 &self.label
205 }
206
207 pub const fn mode(&self) -> TaskMode {
209 self.mode
210 }
211
212 pub fn metadata_value(&self, key: &str) -> Option<&Value> {
214 self.metadata.get(key)
215 }
216
217 pub fn require_value(&self, key: &str) -> Result<&Value, StudyError> {
219 self.metadata_value(key)
220 .ok_or_else(|| StudyError::UnknownTaskMetadata {
221 task: self.key.to_string(),
222 key: key.to_owned(),
223 })
224 }
225
226 pub fn decode_value<T>(&self, key: &str) -> Result<T, StudyError>
228 where
229 T: DeserializeOwned,
230 {
231 T::deserialize(self.require_value(key)?).map_err(|source| StudyError::DecodeTaskMetadata {
232 task: self.key.to_string(),
233 key: key.to_owned(),
234 source,
235 })
236 }
237
238 pub fn metadata_iter(&self) -> impl Iterator<Item = (&str, &Value)> + '_ {
240 self.metadata
241 .iter()
242 .map(|(key, value)| (key.as_str(), value))
243 }
244
245 pub(crate) fn metadata_map(&self) -> Arc<BTreeMap<String, Value>> {
246 Arc::clone(&self.metadata)
247 }
248
249 pub(crate) fn take_workload(&mut self) -> Option<Workload> {
250 self.workload.take()
251 }
252
253 pub(crate) fn has_workload(&self) -> bool {
254 self.workload.is_some()
255 }
256
257 pub(crate) const fn is_completed(&self) -> bool {
258 self.completed
259 }
260
261 fn attach_to_phase(&mut self, phase: PhaseId) {
262 self.key.phase = phase;
263 }
264}
265
266impl fmt::Debug for Task {
267 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
268 formatter
269 .debug_struct("Task")
270 .field("key", &self.key)
271 .field("category", &self.category)
272 .field("label", &self.label)
273 .field("mode", &self.mode)
274 .field("metadata", &self.metadata.len())
275 .finish_non_exhaustive()
276 }
277}
278
279pub struct Phase {
281 id: PhaseId,
282 label: Arc<str>,
283 tasks: Vec<Task>,
284 max_active_tasks: usize,
285 prepared_task_queue_capacity: usize,
286 delay_per_task: Option<Duration>,
287 task_timeout: Option<Duration>,
288 deadline_after: Option<Duration>,
289 dependencies: Vec<PhaseId>,
290 failure_policy: PhaseFailurePolicy,
291 require_confirm: bool,
292}
293
294#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
296#[non_exhaustive]
297pub enum PhaseFailurePolicy {
298 #[default]
300 FailFast,
301 FinishActive,
303}
304
305impl PhaseFailurePolicy {
306 pub const fn as_str(self) -> &'static str {
308 match self {
309 Self::FailFast => "fail-fast",
310 Self::FinishActive => "finish-active",
311 }
312 }
313}
314
315impl Phase {
316 pub fn builder(id: impl Into<PhaseId>, label: impl Into<String>) -> PhaseBuilder {
318 PhaseBuilder {
319 id: id.into(),
320 label: label.into(),
321 tasks: Vec::new(),
322 max_active_tasks: 1,
323 prepared_task_queue_capacity: 1,
324 delay_per_task: None,
325 task_timeout: None,
326 deadline_after: None,
327 dependencies: Vec::new(),
328 failure_policy: PhaseFailurePolicy::FailFast,
329 require_confirm: false,
330 }
331 }
332
333 pub const fn id(&self) -> PhaseId {
335 self.id
336 }
337
338 pub fn label(&self) -> &str {
340 &self.label
341 }
342
343 pub fn tasks(&self) -> &[Task] {
345 &self.tasks
346 }
347
348 pub const fn max_active_tasks(&self) -> usize {
350 self.max_active_tasks
351 }
352
353 pub const fn prepared_task_queue_capacity(&self) -> usize {
355 self.prepared_task_queue_capacity
356 }
357
358 pub const fn delay_per_task(&self) -> Option<Duration> {
360 self.delay_per_task
361 }
362
363 pub const fn task_timeout(&self) -> Option<Duration> {
365 self.task_timeout
366 }
367
368 pub const fn deadline_after(&self) -> Option<Duration> {
370 self.deadline_after
371 }
372
373 pub fn dependencies(&self) -> &[PhaseId] {
375 &self.dependencies
376 }
377
378 pub const fn failure_policy(&self) -> PhaseFailurePolicy {
380 self.failure_policy
381 }
382
383 pub const fn requires_confirmation(&self) -> bool {
386 self.require_confirm
387 }
388
389 pub(crate) fn into_tasks(self) -> Vec<Task> {
390 self.tasks
391 }
392
393 pub fn task(&self, id: &TaskId) -> Option<&Task> {
395 self.tasks.iter().find(|task| task.key.task_id() == id)
396 }
397
398 pub fn unique_task_matching(&self, selector: &TaskSelector) -> Result<&Task, StudyError> {
400 let mut matches = self.tasks.iter().filter(|task| selector.matches(task));
401 let first = matches.next().ok_or_else(|| StudyError::TaskNotFound {
402 selector: selector.to_string(),
403 })?;
404 if let Some(second) = matches.next() {
405 return Err(StudyError::TaskSelectorAmbiguous {
406 selector: selector.to_string(),
407 first: first.key.to_string(),
408 second: second.key.to_string(),
409 });
410 }
411 Ok(first)
412 }
413}
414
415pub struct PhaseBuilder {
417 id: PhaseId,
418 label: String,
419 tasks: Vec<Task>,
420 max_active_tasks: usize,
421 prepared_task_queue_capacity: usize,
422 delay_per_task: Option<Duration>,
423 task_timeout: Option<Duration>,
424 deadline_after: Option<Duration>,
425 dependencies: Vec<PhaseId>,
426 failure_policy: PhaseFailurePolicy,
427 require_confirm: bool,
428}
429
430impl PhaseBuilder {
431 pub fn task(mut self, task: Task) -> Self {
433 self.tasks.push(task);
434 self
435 }
436
437 pub fn tasks<I>(mut self, tasks: I) -> Self
439 where
440 I: IntoIterator<Item = Task>,
441 {
442 self.tasks.extend(tasks);
443 self
444 }
445
446 pub fn max_active_tasks(mut self, maximum: usize) -> Self {
448 self.max_active_tasks = maximum;
449 self
450 }
451
452 pub fn prepared_task_queue_capacity(mut self, capacity: usize) -> Self {
454 self.prepared_task_queue_capacity = capacity;
455 self
456 }
457
458 pub fn delay_per_task(mut self, delay: Duration) -> Self {
463 self.delay_per_task = Some(delay);
464 self
465 }
466
467 pub fn task_timeout(mut self, timeout: Duration) -> Self {
472 self.task_timeout = Some(timeout);
473 self
474 }
475
476 pub fn deadline_after(mut self, deadline: Duration) -> Self {
481 self.deadline_after = Some(deadline);
482 self
483 }
484
485 pub fn depends_on(mut self, dependency: impl Into<PhaseId>) -> Self {
487 self.dependencies.push(dependency.into());
488 self
489 }
490
491 pub fn failure_policy(mut self, policy: PhaseFailurePolicy) -> Self {
493 self.failure_policy = policy;
494 self
495 }
496
497 pub fn require_confirm(mut self, require: bool) -> Self {
502 self.require_confirm = require;
503 self
504 }
505
506 pub fn build(mut self) -> Result<Phase, StudyError> {
508 if self.label.trim().is_empty() {
509 return Err(StudyError::InvalidPhaseLabel { phase: self.id.0 });
510 }
511 if self.tasks.is_empty() {
512 return Err(StudyError::EmptyPhase { phase: self.id.0 });
513 }
514 if self.max_active_tasks == 0 {
515 return Err(StudyError::InvalidPhaseWorkloadLimit { phase: self.id.0 });
516 }
517 if self.prepared_task_queue_capacity == 0 {
518 return Err(StudyError::InvalidPhaseQueueCapacity { phase: self.id.0 });
519 }
520 for (setting, duration) in [
521 ("delay_per_task", self.delay_per_task),
522 ("task_timeout", self.task_timeout),
523 ("deadline_after", self.deadline_after),
524 ] {
525 if duration.is_some_and(|duration| {
526 duration.is_zero() || Instant::now().checked_add(duration).is_none()
527 }) {
528 return Err(StudyError::InvalidPhaseTiming {
529 phase: self.id.0,
530 setting,
531 });
532 }
533 }
534
535 let mut ids = HashSet::with_capacity(self.tasks.len());
536 for task in &mut self.tasks {
537 if task.key.task_id().as_str().trim().is_empty() {
538 return Err(StudyError::InvalidTaskId { phase: self.id.0 });
539 }
540 if task.category.trim().is_empty() {
541 return Err(StudyError::InvalidTaskCategory {
542 task: task.key.task_id().to_string(),
543 });
544 }
545 if !ids.insert(task.key.task_id().clone()) {
546 return Err(StudyError::DuplicateTaskId {
547 phase: self.id.0,
548 task: task.key.task_id().to_string(),
549 });
550 }
551 task.attach_to_phase(self.id);
552 }
553
554 let mut dependencies = HashSet::with_capacity(self.dependencies.len());
555 for dependency in &self.dependencies {
556 if !dependencies.insert(*dependency) || *dependency == self.id {
557 return Err(StudyError::PhaseDependencyCycle { phase: self.id.0 });
558 }
559 }
560
561 Ok(Phase {
562 id: self.id,
563 label: self.label.into(),
564 tasks: self.tasks,
565 max_active_tasks: self.max_active_tasks,
566 prepared_task_queue_capacity: self.prepared_task_queue_capacity,
567 delay_per_task: self.delay_per_task,
568 task_timeout: self.task_timeout,
569 deadline_after: self.deadline_after,
570 dependencies: self.dependencies,
571 failure_policy: self.failure_policy,
572 require_confirm: self.require_confirm,
573 })
574 }
575}
576
577impl fmt::Debug for Phase {
578 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
579 formatter
580 .debug_struct("Phase")
581 .field("id", &self.id)
582 .field("label", &self.label)
583 .field("tasks", &self.tasks.len())
584 .field("max_active_tasks", &self.max_active_tasks)
585 .field(
586 "prepared_task_queue_capacity",
587 &self.prepared_task_queue_capacity,
588 )
589 .field("delay_per_task", &self.delay_per_task)
590 .field("task_timeout", &self.task_timeout)
591 .field("deadline_after", &self.deadline_after)
592 .field("dependencies", &self.dependencies)
593 .field("failure_policy", &self.failure_policy)
594 .field("require_confirm", &self.require_confirm)
595 .finish()
596 }
597}
598
599impl fmt::Debug for PhaseBuilder {
600 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
601 formatter
602 .debug_struct("PhaseBuilder")
603 .field("id", &self.id)
604 .field("label", &self.label)
605 .field("tasks", &self.tasks.len())
606 .field("max_active_tasks", &self.max_active_tasks)
607 .field(
608 "prepared_task_queue_capacity",
609 &self.prepared_task_queue_capacity,
610 )
611 .field("delay_per_task", &self.delay_per_task)
612 .field("task_timeout", &self.task_timeout)
613 .field("deadline_after", &self.deadline_after)
614 .field("dependencies", &self.dependencies)
615 .field("require_confirm", &self.require_confirm)
616 .finish_non_exhaustive()
617 }
618}
619
620#[derive(Clone, Debug, Default)]
622pub struct TaskSelector {
623 phase: Option<PhaseId>,
624 category: Option<Arc<str>>,
625 metadata: Vec<(Box<str>, Value)>,
626}
627
628impl TaskSelector {
629 pub fn new() -> Self {
631 Self::default()
632 }
633
634 pub fn phase(mut self, phase: impl Into<PhaseId>) -> Self {
636 self.phase = Some(phase.into());
637 self
638 }
639
640 pub fn category(mut self, category: impl Into<String>) -> Self {
642 self.category = Some(category.into().into());
643 self
644 }
645
646 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
648 let key = key.into();
649 if let Some((_, current)) = self
650 .metadata
651 .iter_mut()
652 .find(|(candidate, _)| candidate.as_ref() == key)
653 {
654 *current = value.into();
655 } else {
656 self.metadata.push((key.into_boxed_str(), value.into()));
657 }
658 self
659 }
660
661 pub(crate) fn matches(&self, task: &Task) -> bool {
662 self.phase.is_none_or(|phase| task.key.phase == phase)
663 && self
664 .category
665 .as_deref()
666 .is_none_or(|category| task.category_name() == category)
667 && self
668 .metadata
669 .iter()
670 .all(|(key, value)| task.metadata_value(key) == Some(value))
671 }
672}
673
674impl fmt::Display for TaskSelector {
675 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
676 let mut fields = Vec::new();
677 if let Some(phase) = self.phase {
678 fields.push(format!("phase={phase}"));
679 }
680 if let Some(category) = &self.category {
681 fields.push(format!("category={category}"));
682 }
683 fields.extend(
684 self.metadata
685 .iter()
686 .map(|(key, value)| format!("{key}={}", compact_value(value))),
687 );
688 formatter.write_str(&fields.join(", "))
689 }
690}
691
692fn compact_value(value: &Value) -> String {
693 match value {
694 Value::Array(values) => format!("<array:{}:{}>", values.len(), short_hash(value)),
695 Value::Object(values) => format!("<object:{}:{}>", values.len(), short_hash(value)),
696 _ => serde_json::to_string(value)
697 .expect("serde_json::Value always serializes to valid compact JSON"),
698 }
699}
700
701fn short_hash(value: &Value) -> String {
702 let bytes = serde_json::to_vec(value)
703 .expect("serde_json::Value always serializes to valid compact JSON");
704 let digest = Sha256::digest(bytes);
705 digest[..4]
706 .iter()
707 .map(|byte| format!("{byte:02x}"))
708 .collect::<Vec<_>>()
709 .join("")
710}