scientific_workflow/study/
plan.rs1use std::fs::{self, OpenOptions};
4use std::io::Write;
5use std::path::Path;
6
7use serde::Serialize;
8use serde_json::Value;
9
10use super::{Phase, StudyError, TaskMode};
11
12const STUDY_PLAN_FORMAT: &str = "scientific-workflow.study-plan.v1";
13
14#[derive(Clone, Debug, PartialEq, Serialize)]
16pub struct StudyPlan {
17 format: &'static str,
18 phases: Vec<StudyPlanPhase>,
19}
20
21#[derive(Clone, Debug, PartialEq, Serialize)]
22struct StudyPlanPhase {
23 id: u64,
24 label: String,
25 registration_order: usize,
26 dependencies: Vec<u64>,
27 max_active_tasks: usize,
28 prepared_task_queue_capacity: usize,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 delay_per_task_ns: Option<u128>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 task_timeout_ns: Option<u128>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 deadline_after_ns: Option<u128>,
35 failure_policy: &'static str,
36 requires_confirmation: bool,
37 tasks: Vec<StudyPlanTask>,
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize)]
41struct StudyPlanTask {
42 id: String,
43 category: String,
44 label: String,
45 registration_order: usize,
46 mode: &'static str,
47 status: &'static str,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 delay_rank: Option<usize>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 release_offset_ns: Option<u128>,
52 metadata: Value,
53}
54
55impl StudyPlan {
56 pub(crate) fn from_phases(phases: &[Phase]) -> Self {
57 Self {
58 format: STUDY_PLAN_FORMAT,
59 phases: phases
60 .iter()
61 .enumerate()
62 .map(|(registration_order, phase)| {
63 let mut executable_rank = 0_usize;
64 let tasks = phase
65 .tasks()
66 .iter()
67 .enumerate()
68 .map(|(task_order, task)| {
69 let delay_rank = (!task.is_completed()).then(|| {
70 let rank = executable_rank;
71 executable_rank += 1;
72 rank
73 });
74 let release_offset_ns = phase.delay_per_task().and_then(|delay| {
75 delay_rank.map(|rank| delay.as_nanos().saturating_mul(rank as u128))
76 });
77 StudyPlanTask {
78 id: task.id().to_string(),
79 category: task.category_name().to_owned(),
80 label: task.label().to_owned(),
81 registration_order: task_order,
82 mode: match task.mode() {
83 TaskMode::Progress => "progress",
84 TaskMode::OneShot => "one-shot",
85 },
86 status: if task.is_completed() {
87 "completed"
88 } else {
89 "pending"
90 },
91 delay_rank,
92 release_offset_ns,
93 metadata: Value::Object(
94 task.metadata_iter()
95 .map(|(key, value)| (key.to_owned(), value.clone()))
96 .collect(),
97 ),
98 }
99 })
100 .collect();
101 StudyPlanPhase {
102 id: phase.id().get(),
103 label: phase.label().to_owned(),
104 registration_order,
105 dependencies: phase
106 .dependencies()
107 .iter()
108 .map(|dependency| dependency.get())
109 .collect(),
110 max_active_tasks: phase.max_active_tasks(),
111 prepared_task_queue_capacity: phase.prepared_task_queue_capacity(),
112 delay_per_task_ns: phase.delay_per_task().map(|value| value.as_nanos()),
113 task_timeout_ns: phase.task_timeout().map(|value| value.as_nanos()),
114 deadline_after_ns: phase.deadline_after().map(|value| value.as_nanos()),
115 failure_policy: phase.failure_policy().as_str(),
116 requires_confirmation: phase.requires_confirmation(),
117 tasks,
118 }
119 })
120 .collect(),
121 }
122 }
123
124 pub fn to_pretty_json(&self) -> Result<Vec<u8>, StudyError> {
126 let mut bytes = serde_json::to_vec_pretty(self)
127 .map_err(|source| StudyError::SerializeStudyPlan { source })?;
128 bytes.push(b'\n');
129 Ok(bytes)
130 }
131
132 pub fn write_json(&self, path: impl AsRef<Path>) -> Result<(), StudyError> {
134 let path = path.as_ref();
135 let bytes = self.to_pretty_json()?;
136 match OpenOptions::new().write(true).create_new(true).open(path) {
137 Ok(mut file) => file
138 .write_all(&bytes)
139 .and_then(|()| file.sync_all())
140 .map_err(|source| StudyError::WriteStudyPlan {
141 path: path.to_path_buf(),
142 source,
143 }),
144 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
145 let existing = fs::read(path).map_err(|source| StudyError::WriteStudyPlan {
146 path: path.to_path_buf(),
147 source,
148 })?;
149 if existing == bytes {
150 Ok(())
151 } else {
152 Err(StudyError::StudyPlanConflict {
153 path: path.to_path_buf(),
154 })
155 }
156 }
157 Err(source) => Err(StudyError::WriteStudyPlan {
158 path: path.to_path_buf(),
159 source,
160 }),
161 }
162 }
163}