scientific_workflow/execution/
scope.rs1use std::fs;
2use std::path::{Component, Path, PathBuf};
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use crate::clock::utc_now_rfc3339;
6
7use super::error::ExecutionScopeError;
8
9static EXECUTION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct ExecutionScope {
14 directory: PathBuf,
15 created_at_utc: Option<String>,
16}
17
18impl ExecutionScope {
19 pub fn open_or_create(recording_root: impl AsRef<Path>) -> Result<Self, ExecutionScopeError> {
25 let directory = recording_root.as_ref().to_path_buf();
26 fs::create_dir_all(&directory).map_err(|source| ExecutionScopeError::Io {
27 operation: "create deterministic",
28 path: directory.clone(),
29 source,
30 })?;
31 Ok(Self {
32 directory,
33 created_at_utc: None,
34 })
35 }
36
37 pub fn create_generated(recording_root: impl AsRef<Path>) -> Result<Self, ExecutionScopeError> {
42 let recording_root = recording_root.as_ref();
43 fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
44 operation: "create recording root for",
45 path: recording_root.to_path_buf(),
46 source,
47 })?;
48 let created_at_utc =
49 utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
50 let compact_timestamp = compact_timestamp(&created_at_utc);
51 for _ in 0..1024 {
52 let sequence = EXECUTION_SEQUENCE.fetch_add(1, Ordering::Relaxed);
53 let name = format!(
54 "execution-{compact_timestamp}-{}-{sequence}",
55 std::process::id()
56 );
57 let directory = recording_root.join(name);
58 match fs::create_dir(&directory) {
59 Ok(()) => {
60 return Ok(Self {
61 directory,
62 created_at_utc: Some(created_at_utc),
63 });
64 }
65 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => continue,
66 Err(source) => {
67 return Err(ExecutionScopeError::Io {
68 operation: "create generated",
69 path: directory,
70 source,
71 });
72 }
73 }
74 }
75 Err(ExecutionScopeError::IdentityExhausted {
76 root: recording_root.to_path_buf(),
77 })
78 }
79
80 pub fn create_named(
82 recording_root: impl AsRef<Path>,
83 name: impl Into<String>,
84 ) -> Result<Self, ExecutionScopeError> {
85 let recording_root = recording_root.as_ref();
86 let name = name.into();
87 validate_name(&name)?;
88 let created_at_utc =
89 utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
90 fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
91 operation: "create recording root for",
92 path: recording_root.to_path_buf(),
93 source,
94 })?;
95 let directory = recording_root.join(&name);
96 fs::create_dir(&directory).map_err(|source| ExecutionScopeError::Io {
97 operation: "create named",
98 path: directory.clone(),
99 source,
100 })?;
101 Ok(Self {
102 directory,
103 created_at_utc: Some(created_at_utc),
104 })
105 }
106
107 pub fn open_existing(directory: impl Into<PathBuf>) -> Result<Self, ExecutionScopeError> {
109 let directory = directory.into();
110 let metadata = fs::metadata(&directory).map_err(|source| ExecutionScopeError::Io {
111 operation: "inspect existing",
112 path: directory.clone(),
113 source,
114 })?;
115 if !metadata.is_dir() {
116 return Err(ExecutionScopeError::Io {
117 operation: "open non-directory",
118 path: directory.clone(),
119 source: std::io::Error::new(
120 std::io::ErrorKind::NotADirectory,
121 "execution scope must be a directory",
122 ),
123 });
124 }
125 Ok(Self {
126 directory,
127 created_at_utc: None,
128 })
129 }
130
131 pub fn directory(&self) -> &Path {
133 &self.directory
134 }
135
136 pub fn created_at_utc(&self) -> Option<&str> {
142 self.created_at_utc.as_deref()
143 }
144
145 pub fn task_recording_directory(&self, task_ordinal: u64) -> PathBuf {
150 self.directory.join(format!("task-{task_ordinal:06}"))
151 }
152
153 pub fn named_task_recording_directory(
155 &self,
156 name: &str,
157 ) -> Result<PathBuf, ExecutionScopeError> {
158 validate_relative_name(name)?;
159 Ok(self.directory.join(name))
160 }
161}
162
163fn validate_name(name: &str) -> Result<(), ExecutionScopeError> {
165 let path = Path::new(name);
166 let mut components = path.components();
167 let valid = !name.trim().is_empty()
168 && matches!(components.next(), Some(Component::Normal(_)))
169 && components.next().is_none();
170 if valid {
171 Ok(())
172 } else {
173 Err(ExecutionScopeError::InvalidName {
174 name: name.to_owned(),
175 })
176 }
177}
178
179fn validate_relative_name(name: &str) -> Result<(), ExecutionScopeError> {
181 let path = Path::new(name);
182 let valid = !name.trim().is_empty()
183 && path.components().next().is_some()
184 && path
185 .components()
186 .all(|component| matches!(component, Component::Normal(_)));
187 if valid {
188 Ok(())
189 } else {
190 Err(ExecutionScopeError::InvalidName {
191 name: name.to_owned(),
192 })
193 }
194}
195
196fn compact_timestamp(timestamp: &str) -> String {
198 timestamp
199 .chars()
200 .filter(|character| character.is_ascii_alphanumeric())
201 .collect()
202}