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 create_generated(recording_root: impl AsRef<Path>) -> Result<Self, ExecutionScopeError> {
24 let recording_root = recording_root.as_ref();
25 fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
26 operation: "create recording root for",
27 path: recording_root.to_path_buf(),
28 source,
29 })?;
30 let created_at_utc =
31 utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
32 let compact_timestamp = compact_timestamp(&created_at_utc);
33 for _ in 0..1024 {
34 let sequence = EXECUTION_SEQUENCE.fetch_add(1, Ordering::Relaxed);
35 let name = format!(
36 "execution-{compact_timestamp}-{}-{sequence}",
37 std::process::id()
38 );
39 let directory = recording_root.join(name);
40 match fs::create_dir(&directory) {
41 Ok(()) => {
42 return Ok(Self {
43 directory,
44 created_at_utc: Some(created_at_utc),
45 });
46 }
47 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => continue,
48 Err(source) => {
49 return Err(ExecutionScopeError::Io {
50 operation: "create generated",
51 path: directory,
52 source,
53 });
54 }
55 }
56 }
57 Err(ExecutionScopeError::IdentityExhausted {
58 root: recording_root.to_path_buf(),
59 })
60 }
61
62 pub fn create_named(
64 recording_root: impl AsRef<Path>,
65 name: impl Into<String>,
66 ) -> Result<Self, ExecutionScopeError> {
67 let recording_root = recording_root.as_ref();
68 let name = name.into();
69 validate_name(&name)?;
70 let created_at_utc =
71 utc_now_rfc3339().map_err(|source| ExecutionScopeError::Timestamp { source })?;
72 fs::create_dir_all(recording_root).map_err(|source| ExecutionScopeError::Io {
73 operation: "create recording root for",
74 path: recording_root.to_path_buf(),
75 source,
76 })?;
77 let directory = recording_root.join(&name);
78 fs::create_dir(&directory).map_err(|source| ExecutionScopeError::Io {
79 operation: "create named",
80 path: directory.clone(),
81 source,
82 })?;
83 Ok(Self {
84 directory,
85 created_at_utc: Some(created_at_utc),
86 })
87 }
88
89 pub fn open_existing(directory: impl Into<PathBuf>) -> Result<Self, ExecutionScopeError> {
91 let directory = directory.into();
92 let metadata = fs::metadata(&directory).map_err(|source| ExecutionScopeError::Io {
93 operation: "inspect existing",
94 path: directory.clone(),
95 source,
96 })?;
97 if !metadata.is_dir() {
98 return Err(ExecutionScopeError::Io {
99 operation: "open non-directory",
100 path: directory.clone(),
101 source: std::io::Error::new(
102 std::io::ErrorKind::NotADirectory,
103 "execution scope must be a directory",
104 ),
105 });
106 }
107 Ok(Self {
108 directory,
109 created_at_utc: None,
110 })
111 }
112
113 pub fn directory(&self) -> &Path {
115 &self.directory
116 }
117
118 pub fn created_at_utc(&self) -> Option<&str> {
124 self.created_at_utc.as_deref()
125 }
126
127 pub fn task_recording_directory(&self, task_ordinal: u64) -> PathBuf {
132 self.directory.join(format!("task-{task_ordinal:06}"))
133 }
134}
135
136fn validate_name(name: &str) -> Result<(), ExecutionScopeError> {
138 let path = Path::new(name);
139 let mut components = path.components();
140 let valid = !name.trim().is_empty()
141 && matches!(components.next(), Some(Component::Normal(_)))
142 && components.next().is_none();
143 if valid {
144 Ok(())
145 } else {
146 Err(ExecutionScopeError::InvalidName {
147 name: name.to_owned(),
148 })
149 }
150}
151
152fn compact_timestamp(timestamp: &str) -> String {
154 timestamp
155 .chars()
156 .filter(|character| character.is_ascii_alphanumeric())
157 .collect()
158}