scientific_workflow/
execution.rs1use std::fs;
10use std::path::{Component, Path, PathBuf};
11use std::sync::atomic::{AtomicU64, Ordering};
12
13use thiserror::Error;
14
15use crate::clock::utc_now_rfc3339;
16
17static EXECUTION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
19
20#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum ExecutionScopeError {
24 #[error("invalid execution scope name `{name}`")]
26 InvalidName {
27 name: String,
29 },
30 #[error("failed to format execution scope creation timestamp")]
32 Timestamp {
33 #[source]
35 source: time::error::Format,
36 },
37 #[error("failed to {operation} execution scope at `{path}`")]
39 Io {
40 operation: &'static str,
42 path: PathBuf,
44 #[source]
46 source: std::io::Error,
47 },
48 #[error("could not allocate a unique execution scope beneath `{root}`")]
50 IdentityExhausted {
51 root: PathBuf,
53 },
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct ExecutionScope {
59 directory: PathBuf,
60 created_at_utc: Option<String>,
61}
62
63impl ExecutionScope {
64 pub fn create_generated(recording_root: impl AsRef<Path>) -> Result<Self, ExecutionScopeError> {
69 let recording_root = recording_root.as_ref();
70 fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
71 operation: "create recording root for",
72 path: recording_root.to_path_buf(),
73 source,
74 })?;
75 let created_at_utc =
76 utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
77 let compact_timestamp = compact_timestamp(&created_at_utc);
78 for _ in 0..1024 {
79 let sequence = EXECUTION_SEQUENCE.fetch_add(1, Ordering::Relaxed);
80 let name = format!(
81 "execution-{compact_timestamp}-{}-{sequence}",
82 std::process::id()
83 );
84 let directory = recording_root.join(name);
85 match fs::create_dir(&directory) {
86 Ok(()) => {
87 return Ok(Self {
88 directory,
89 created_at_utc: Some(created_at_utc),
90 });
91 }
92 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => continue,
93 Err(source) => {
94 return Err(ExecutionScopeError::Io {
95 operation: "create generated",
96 path: directory,
97 source,
98 });
99 }
100 }
101 }
102 Err(ExecutionScopeError::IdentityExhausted {
103 root: recording_root.to_path_buf(),
104 })
105 }
106
107 pub fn create_named(
109 recording_root: impl AsRef<Path>,
110 name: impl Into<String>,
111 ) -> Result<Self, ExecutionScopeError> {
112 let recording_root = recording_root.as_ref();
113 let name = name.into();
114 validate_name(&name)?;
115 let created_at_utc =
116 utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
117 fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
118 operation: "create recording root for",
119 path: recording_root.to_path_buf(),
120 source,
121 })?;
122 let directory = recording_root.join(&name);
123 fs::create_dir(&directory).map_err(|source| ExecutionScopeError::Io {
124 operation: "create named",
125 path: directory.clone(),
126 source,
127 })?;
128 Ok(Self {
129 directory,
130 created_at_utc: Some(created_at_utc),
131 })
132 }
133
134 pub fn open_existing(directory: impl Into<PathBuf>) -> Result<Self, ExecutionScopeError> {
136 let directory = directory.into();
137 let metadata = fs::metadata(&directory).map_err(|source| ExecutionScopeError::Io {
138 operation: "inspect existing",
139 path: directory.clone(),
140 source,
141 })?;
142 if !metadata.is_dir() {
143 return Err(ExecutionScopeError::Io {
144 operation: "open non-directory",
145 path: directory.clone(),
146 source: std::io::Error::new(
147 std::io::ErrorKind::NotADirectory,
148 "execution scope must be a directory",
149 ),
150 });
151 }
152 Ok(Self {
153 directory,
154 created_at_utc: None,
155 })
156 }
157
158 pub fn directory(&self) -> &Path {
160 &self.directory
161 }
162
163 pub fn created_at_utc(&self) -> Option<&str> {
169 self.created_at_utc.as_deref()
170 }
171
172 pub fn task_recording_directory(&self, task_ordinal: u64) -> PathBuf {
177 self.directory.join(format!("task-{task_ordinal:06}"))
178 }
179}
180
181fn validate_name(name: &str) -> Result<(), ExecutionScopeError> {
183 let path = Path::new(name);
184 let mut components = path.components();
185 let valid = !name.trim().is_empty()
186 && matches!(components.next(), Some(Component::Normal(_)))
187 && components.next().is_none();
188 if valid {
189 Ok(())
190 } else {
191 Err(ExecutionScopeError::InvalidName {
192 name: name.to_owned(),
193 })
194 }
195}
196
197fn compact_timestamp(timestamp: &str) -> String {
199 timestamp
200 .chars()
201 .filter(|character| character.is_ascii_alphanumeric())
202 .collect()
203}