ic_backup/ops/persistence/command_lifetime_lock/
mod.rs1use crate::model::command_custody::{CommandCustodyRecord, CommandCustodyRecordError};
4#[cfg(unix)]
5use ic_host_process::custody::{CommandCustody, CustodyError, FileIdentity, QuiescenceGuard};
6#[cfg(unix)]
7use std::time::Duration;
8use std::{
9 fs, io,
10 path::{Path, PathBuf},
11 process::{Child, Command},
12};
13use thiserror::Error;
14
15pub const COMMAND_CUSTODY_DESCRIPTOR_ENV: &str = "IC_BACKUP_COMMAND_CUSTODY_FD";
17
18#[derive(Debug)]
23pub struct CommandLifetimeLock {
24 #[cfg(unix)]
25 custody: CommandCustody,
26 path: PathBuf,
27 record: CommandCustodyRecord,
28}
29
30impl CommandLifetimeLock {
31 pub fn acquire(
35 journal: &Path,
36 operation_sequence: u64,
37 ) -> Result<Self, CommandLifetimeLockError> {
38 let journal = super::journal_path::resolve_journal(
39 journal,
40 || CommandLifetimeLockError::InvalidJournal {
41 path: journal.to_path_buf(),
42 },
43 |path| CommandLifetimeLockError::InvalidJournal { path },
44 )?;
45 let path = lock_path(&journal, operation_sequence);
46 #[cfg(unix)]
47 {
48 let custody =
49 CommandCustody::acquire(&path).map_err(|error| project_error(&path, error))?;
50 let identity = custody.identity();
51 let record = CommandCustodyRecord::new(
52 journal,
53 operation_sequence,
54 identity.device,
55 identity.inode,
56 )?;
57 Ok(Self {
58 custody,
59 path,
60 record,
61 })
62 }
63 #[cfg(not(unix))]
64 {
65 let _ = path;
66 Err(io::Error::from(io::ErrorKind::Unsupported).into())
67 }
68 }
69
70 #[must_use]
72 pub fn record(&self) -> &CommandCustodyRecord {
73 &self.record
74 }
75
76 #[must_use]
78 pub fn path(&self) -> &Path {
79 &self.path
80 }
81
82 pub fn spawn(&mut self, command: Command) -> Result<Child, CommandLifetimeLockError> {
89 #[cfg(unix)]
90 {
91 self.custody
92 .spawn(command, COMMAND_CUSTODY_DESCRIPTOR_ENV)
93 .map_err(|error| project_error(&self.path, error))
94 }
95 #[cfg(not(unix))]
96 {
97 let _ = command;
98 Err(io::Error::from(io::ErrorKind::Unsupported).into())
99 }
100 }
101
102 pub fn finish(self) -> Result<CommandQuiescenceGuard, CommandLifetimeLockError> {
109 #[cfg(unix)]
110 {
111 let guard = self
112 .custody
113 .finish(Duration::from_millis(250), Duration::from_millis(5))
114 .map_err(|error| project_error(&self.path, error))?;
115 Ok(CommandQuiescenceGuard {
116 _guard: guard,
117 record: self.record,
118 })
119 }
120 #[cfg(not(unix))]
121 {
122 Err(io::Error::from(io::ErrorKind::Unsupported).into())
123 }
124 }
125}
126
127#[derive(Debug)]
132pub struct CommandQuiescenceGuard {
133 #[cfg(unix)]
134 _guard: QuiescenceGuard,
135 record: CommandCustodyRecord,
136}
137
138impl CommandQuiescenceGuard {
139 pub fn acquire(expected: &CommandCustodyRecord) -> Result<Self, CommandLifetimeLockError> {
143 #[cfg(unix)]
144 {
145 let path = lock_path(expected.journal(), expected.operation_sequence());
146 let identity = FileIdentity {
147 device: expected.device(),
148 inode: expected.inode(),
149 };
150 let guard = QuiescenceGuard::acquire(&path, identity)
151 .map_err(|error| project_error(&path, error))?;
152 Ok(Self {
153 _guard: guard,
154 record: expected.clone(),
155 })
156 }
157 #[cfg(not(unix))]
158 {
159 let _ = expected;
160 Err(io::Error::from(io::ErrorKind::Unsupported).into())
161 }
162 }
163
164 #[must_use]
166 pub fn record(&self) -> &CommandCustodyRecord {
167 &self.record
168 }
169}
170
171fn lock_path(journal: &Path, operation_sequence: u64) -> PathBuf {
172 let mut path = journal.as_os_str().to_os_string();
173 path.push(format!(".command-{operation_sequence}.lock"));
174 PathBuf::from(path)
175}
176
177#[cfg(unix)]
178fn project_error(path: &Path, error: CustodyError) -> CommandLifetimeLockError {
179 use ic_host_fs::durable::RegularFileLockError;
180 match error {
181 CustodyError::InFlight { path } => CommandLifetimeLockError::InFlight { path },
182 CustodyError::IdentityChanged { path } => {
183 CommandLifetimeLockError::IdentityChanged { path }
184 }
185 CustodyError::AlreadyDispatched { path } => {
186 CommandLifetimeLockError::AlreadyDispatched { path }
187 }
188 CustodyError::Io(error) => CommandLifetimeLockError::Io(error),
189 CustodyError::Lock(RegularFileLockError::NotRegular) => {
190 let kind = match fs::symlink_metadata(path) {
191 Ok(metadata) if metadata.file_type().is_symlink() => "Symlink",
192 Ok(metadata) if metadata.is_dir() => "Directory",
193 _ => "Special",
194 };
195 CommandLifetimeLockError::UnsafeEntry {
196 path: path.to_path_buf(),
197 kind: kind.into(),
198 }
199 }
200 CustodyError::Lock(error) => CommandLifetimeLockError::Io(error.into()),
201 }
202}
203
204#[derive(Debug, Error)]
206pub enum CommandLifetimeLockError {
207 #[error("command custody remains in flight: {path:?}")]
209 InFlight {
210 path: PathBuf,
212 },
213 #[error("command custody identity changed: {path:?}")]
215 IdentityChanged {
216 path: PathBuf,
218 },
219 #[error("command spawn already attempted: {path:?}")]
221 AlreadyDispatched {
222 path: PathBuf,
224 },
225 #[error("unsafe command custody entry at {path:?}: {kind}")]
227 UnsafeEntry {
228 path: PathBuf,
230 kind: String,
232 },
233 #[error("invalid command journal location: {path:?}")]
235 InvalidJournal {
236 path: PathBuf,
238 },
239 #[error(transparent)]
241 Record(#[from] CommandCustodyRecordError),
242 #[error(transparent)]
244 Io(#[from] io::Error),
245}
246
247#[cfg(all(test, unix))]
248mod tests;