scientific_workflow/runtime/
execution_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, RuntimeError, TaskDisplayKind};
11
12const EXECUTION_PLAN_FORMAT: &str = "scientific-workflow.execution-plan.v1";
13
14#[derive(Clone, Debug, PartialEq, Serialize)]
16pub struct ExecutionPlan {
17 format: &'static str,
18 phases: Vec<ExecutionPlanPhase>,
19}
20
21#[derive(Clone, Debug, PartialEq, Serialize)]
22struct ExecutionPlanPhase {
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<ExecutionPlanTask>,
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize)]
41struct ExecutionPlanTask {
42 id: String,
43 kind: String,
44 label: String,
45 registration_order: usize,
46 configuration_ordinal: u64,
47 display_kind: &'static str,
48 status: &'static str,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 delay_rank: Option<usize>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 release_offset_ns: Option<u128>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 display_parameters: Option<Vec<String>>,
55 configuration: Value,
56}
57
58impl ExecutionPlan {
59 pub(crate) fn from_phases(phases: &[Phase]) -> Self {
60 Self {
61 format: EXECUTION_PLAN_FORMAT,
62 phases: phases
63 .iter()
64 .enumerate()
65 .map(|(registration_order, phase)| {
66 let mut executable_rank = 0_usize;
67 let tasks = phase
68 .tasks()
69 .iter()
70 .enumerate()
71 .map(|(task_order, task)| {
72 let delay_rank = (!task.is_reused()).then(|| {
73 let rank = executable_rank;
74 executable_rank += 1;
75 rank
76 });
77 let release_offset_ns = phase.delay_per_task().and_then(|delay| {
78 delay_rank.map(|rank| delay.as_nanos().saturating_mul(rank as u128))
79 });
80 ExecutionPlanTask {
81 id: task.id().to_string(),
82 kind: task.kind().to_owned(),
83 label: task.label().to_owned(),
84 registration_order: task_order,
85 configuration_ordinal: task.configuration_ordinal(),
86 display_kind: match task.display_kind() {
87 TaskDisplayKind::Progress => "progress",
88 TaskDisplayKind::Activity => "activity",
89 },
90 status: if task.is_reused() {
91 "reused"
92 } else {
93 "pending"
94 },
95 delay_rank,
96 release_offset_ns,
97 display_parameters: task
98 .display_keys()
99 .map(|keys| keys.iter().map(|key| key.to_string()).collect()),
100 configuration: task.configuration().resolved_json(),
101 }
102 })
103 .collect();
104 ExecutionPlanPhase {
105 id: phase.id().get(),
106 label: phase.label().to_owned(),
107 registration_order,
108 dependencies: phase
109 .dependencies()
110 .iter()
111 .map(|dependency| dependency.get())
112 .collect(),
113 max_active_tasks: phase.max_active_tasks(),
114 prepared_task_queue_capacity: phase.prepared_task_queue_capacity(),
115 delay_per_task_ns: phase.delay_per_task().map(|value| value.as_nanos()),
116 task_timeout_ns: phase.task_timeout().map(|value| value.as_nanos()),
117 deadline_after_ns: phase.deadline_after().map(|value| value.as_nanos()),
118 failure_policy: phase.failure_policy().as_str(),
119 requires_confirmation: phase.requires_confirmation(),
120 tasks,
121 }
122 })
123 .collect(),
124 }
125 }
126
127 pub fn to_pretty_json(&self) -> Result<Vec<u8>, RuntimeError> {
129 let mut bytes = serde_json::to_vec_pretty(self)
130 .map_err(|source| RuntimeError::SerializeExecutionPlan { source })?;
131 bytes.push(b'\n');
132 Ok(bytes)
133 }
134
135 pub fn write_json(&self, path: impl AsRef<Path>) -> Result<(), RuntimeError> {
137 let path = path.as_ref();
138 let bytes = self.to_pretty_json()?;
139 match OpenOptions::new().write(true).create_new(true).open(path) {
140 Ok(mut file) => file
141 .write_all(&bytes)
142 .and_then(|()| file.sync_all())
143 .map_err(|source| RuntimeError::WriteExecutionPlan {
144 path: path.to_path_buf(),
145 source,
146 }),
147 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
148 let existing =
149 fs::read(path).map_err(|source| RuntimeError::WriteExecutionPlan {
150 path: path.to_path_buf(),
151 source,
152 })?;
153 if existing == bytes {
154 Ok(())
155 } else {
156 Err(RuntimeError::ExecutionPlanConflict {
157 path: path.to_path_buf(),
158 })
159 }
160 }
161 Err(source) => Err(RuntimeError::WriteExecutionPlan {
162 path: path.to_path_buf(),
163 source,
164 }),
165 }
166 }
167}