Skip to main content

scientific_workflow/study/
task.rs

1//! Task-local execution context and workload contract.
2
3use std::error::Error;
4
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7
8use super::error::StudyError;
9use super::phase::{Task, TaskId, TaskKey};
10use super::renderer::{OneShotTaskHandle, TaskProgressHandle};
11
12/// Error-erased result returned by one task-owned workload.
13pub type TaskResult = Result<(), Box<dyn Error + Send + Sync + 'static>>;
14
15pub(crate) type Workload = Box<dyn FnOnce(&TaskContext) -> TaskResult + Send + 'static>;
16
17enum TaskHandle {
18    Progress(TaskProgressHandle),
19    OneShot(OneShotTaskHandle),
20}
21
22/// Read-only task identity plus reporting and cancellation.
23///
24/// The context deliberately has no filesystem, storage, artifact, network,
25/// subprocess, or machine-resource operations. The workload owns all such
26/// effects directly.
27pub struct TaskContext {
28    task: Task,
29    handle: TaskHandle,
30}
31
32impl TaskContext {
33    pub(crate) fn progress(task: Task, progress: TaskProgressHandle) -> Self {
34        Self {
35            task,
36            handle: TaskHandle::Progress(progress),
37        }
38    }
39
40    pub(crate) fn one_shot(task: Task, one_shot: OneShotTaskHandle) -> Self {
41        Self {
42            task,
43            handle: TaskHandle::OneShot(one_shot),
44        }
45    }
46
47    /// Borrows the complete immutable task declaration.
48    pub fn task(&self) -> &Task {
49        &self.task
50    }
51
52    /// Borrows the exact phase-qualified task key.
53    pub fn key(&self) -> &TaskKey {
54        self.task.key()
55    }
56
57    /// Borrows the phase-local task ID.
58    pub fn id(&self) -> &TaskId {
59        self.task.id()
60    }
61
62    /// Borrows the application-defined task category.
63    pub fn category(&self) -> &str {
64        self.task.category_name()
65    }
66
67    /// Borrows one application-defined metadata value.
68    pub fn metadata(&self, key: &str) -> Option<&Value> {
69        self.task.metadata_value(key)
70    }
71
72    /// Decodes one required metadata value.
73    pub fn decode_metadata<T>(&self, key: &str) -> Result<T, StudyError>
74    where
75        T: DeserializeOwned,
76    {
77        self.task.decode_metadata(key)
78    }
79
80    /// Sets or replaces the target iteration of a progress task.
81    pub fn set_target_iteration(&self, target: u64) -> Result<(), StudyError> {
82        self.required_progress()?.set_target_iteration(target)
83    }
84
85    /// Synchronizes a progress task to the authoritative scientific iteration.
86    pub fn set_iteration(&self, iteration: u64) -> Result<(), StudyError> {
87        self.required_progress()?.set_iteration(iteration)
88    }
89
90    /// Synchronizes iteration and reports whether work should continue.
91    pub fn should_continue(&self, iteration: u64) -> Result<bool, StudyError> {
92        self.required_progress()?.should_continue(iteration)
93    }
94
95    /// Reports whether the study requested cooperative cancellation.
96    pub fn is_cancelled(&self) -> bool {
97        match &self.handle {
98            TaskHandle::Progress(progress) => progress.is_cancelled(),
99            TaskHandle::OneShot(one_shot) => one_shot.is_cancelled(),
100        }
101    }
102
103    /// Updates the human-readable task detail.
104    pub fn set_detail(&self, detail: impl Into<String>) {
105        let detail = detail.into();
106        match &self.handle {
107            TaskHandle::Progress(progress) => progress.set_detail(detail),
108            TaskHandle::OneShot(one_shot) => one_shot.set_detail(detail),
109        }
110    }
111
112    /// Sends a task-scoped display message.
113    pub fn report(&self, message: impl Into<String>) -> Result<(), StudyError> {
114        let message = message.into();
115        match &self.handle {
116            TaskHandle::Progress(progress) => progress.report(message),
117            TaskHandle::OneShot(one_shot) => one_shot.report(message),
118        }
119    }
120
121    pub(crate) fn complete(self) -> Result<(), StudyError> {
122        match self.handle {
123            TaskHandle::Progress(progress) => progress.complete(None),
124            TaskHandle::OneShot(one_shot) => {
125                one_shot.complete();
126                Ok(())
127            }
128        }
129    }
130
131    pub(crate) fn fail(self, reason: impl Into<String>) {
132        let reason = reason.into();
133        match self.handle {
134            TaskHandle::Progress(progress) => progress.fail(reason),
135            TaskHandle::OneShot(one_shot) => one_shot.fail(reason),
136        }
137    }
138
139    pub(crate) fn cancel(self, reason: impl Into<String>) {
140        let reason = reason.into();
141        match self.handle {
142            TaskHandle::Progress(progress) => progress.cancel(reason),
143            TaskHandle::OneShot(one_shot) => one_shot.cancel(reason),
144        }
145    }
146
147    fn required_progress(&self) -> Result<&TaskProgressHandle, StudyError> {
148        match &self.handle {
149            TaskHandle::Progress(progress) => Ok(progress),
150            TaskHandle::OneShot(_) => Err(StudyError::TaskModeMismatch {
151                task: self.key().to_string(),
152                requested: "progress",
153                actual: "one-shot",
154            }),
155        }
156    }
157}
158
159impl std::fmt::Debug for TaskContext {
160    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        formatter
162            .debug_struct("TaskContext")
163            .field("key", self.key())
164            .field("category", &self.category())
165            .field("mode", &self.task.mode())
166            .finish_non_exhaustive()
167    }
168}