git_policy_cli/
recovery_scan.rs1use super::diagnostic_log::debug;
18use super::owner_lock::{
20 DEFAULT_POLL_DELAY, OwnerLock, SilentWait, acquire_owner_lock, retire_lock_of_dead_owner,
21};
22use super::recovery_error::{RecoveryError, io_failure};
24use super::recovery_evidence::{entered_landing, has_landing_record};
26use super::recovery_inspect::{
28 InspectedEntry, OwnerEvidence, inspect_registry_entry, oldest_first,
29};
30use super::recovery_landing::{RecoveryAction, RecoveryPlace, recover_dead_transaction};
32use super::transaction_owner::OwnerLiveness;
34use super::transaction_registry::{
36 EntryKind, LANDING_LOCK_NAME, RESERVATION_LOCK_NAME, RegistryEntry, list_transaction_entries,
37};
38use std::path::{Path, PathBuf};
40
41#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct RecoveryOutcome {
52 pub directory: PathBuf,
54 pub action: RecoveryAction,
57}
58
59pub fn recover_inspected_entry(
71 place: &RecoveryPlace,
72 inspected: &InspectedEntry,
73) -> Result<RecoveryAction, RecoveryError> {
74 let path: &Path = inspected.entry.path.as_path();
75 match &inspected.owner {
76 OwnerEvidence::Vanished => {
77 debug(
78 "recoverInspectedEntry",
79 format!(
80 "transaction finished while recovery listed it: {}",
81 path.display()
82 )
83 .as_str(),
84 );
85 return Ok(RecoveryAction::Vanished);
86 }
87 OwnerEvidence::Unattributed => {
88 debug(
89 "recoverInspectedEntry",
90 format!(
91 "staging candidate without a complete owner record left in place: {}",
92 path.display()
93 )
94 .as_str(),
95 );
96 return Ok(RecoveryAction::StagingUnattributed);
97 }
98 OwnerEvidence::Owned(record, OwnerLiveness::Alive) => {
99 debug(
100 "recoverInspectedEntry",
101 format!(
102 "transaction owner {} is active; skipping {}",
103 record.owner_pid,
104 path.display()
105 )
106 .as_str(),
107 );
108 return Ok(RecoveryAction::OwnerActive);
109 }
110 OwnerEvidence::Owned(record, OwnerLiveness::Dead) => {
111 if inspected.entry.kind == EntryKind::Staging {
112 debug(
115 "recoverInspectedEntry",
116 format!(
117 "dead owner stopped before publishing; staging candidate kept: {}",
118 path.display()
119 )
120 .as_str(),
121 );
122 return Ok(RecoveryAction::StagingRetained);
123 }
124 return recover_dead_transaction(
125 place,
126 path,
127 inspected.entry.transaction_id.as_str(),
128 record.owner_pid,
129 );
130 }
131 }
132}
133
134fn needs_landing_lock(inspected: &InspectedEntry) -> Result<bool, RecoveryError> {
144 if inspected.entry.kind != EntryKind::Transaction || inspected.dead_owner().is_none() {
145 return Ok(false);
146 }
147 return has_landing_record(inspected.entry.path.as_path());
148}
149
150pub fn recover_dead_landings(
165 place: &RecoveryPlace,
166 root: &Path,
167) -> Result<Vec<RecoveryOutcome>, RecoveryError> {
168 let mut inspected: Vec<InspectedEntry> = Vec::new();
169 for entry in list_transaction_entries(root)? {
170 if entry.kind == EntryKind::Transaction {
171 inspected.push(inspect_registry_entry(&entry)?);
172 }
173 }
174 let mut outcomes: Vec<RecoveryOutcome> = Vec::new();
176 for item in oldest_first(inspected) {
177 let Some(record) = item.dead_owner() else {
178 continue;
179 };
180 if !entered_landing(item.entry.path.as_path())? {
181 continue;
182 }
183 debug(
184 "recoverDeadLandings",
185 format!("recovering dead landing {}", item.entry.path.display()).as_str(),
186 );
187 let action: RecoveryAction = recover_dead_transaction(
188 place,
189 item.entry.path.as_path(),
190 item.entry.transaction_id.as_str(),
191 record.owner_pid,
192 )?;
193 outcomes.push(RecoveryOutcome {
194 directory: item.entry.path.clone(),
195 action,
196 });
197 }
198 return Ok(outcomes);
199}
200
201pub fn acquire_registry_lock(root: &Path, name: &str) -> Result<OwnerLock, RecoveryError> {
212 let directory: PathBuf = root.join(name);
213 let mut silent: SilentWait = SilentWait;
215 match acquire_owner_lock(directory.as_path(), DEFAULT_POLL_DELAY, &mut silent) {
216 Ok(lock) => return Ok(lock),
217 Err(error) => return Err(RecoveryError(error.to_string())),
218 }
219}
220
221fn recovered_already(outcomes: &[RecoveryOutcome], path: &Path) -> bool {
231 for outcome in outcomes {
232 if outcome.directory == path {
233 return true;
234 }
235 }
236 return false;
237}
238
239pub fn recover_registered_transactions(
256 place: &RecoveryPlace,
257 root: &Path,
258) -> Result<Vec<RecoveryOutcome>, RecoveryError> {
259 let entries: Vec<RegistryEntry> = list_transaction_entries(root)?;
260 let mut inspected: Vec<InspectedEntry> = Vec::new();
261 let mut retired: Vec<PathBuf> = Vec::new();
262 for entry in &entries {
263 if entry.kind == EntryKind::Retired {
264 retired.push(entry.path.clone());
265 } else {
266 inspected.push(inspect_registry_entry(entry)?);
267 }
268 }
269 let ordered: Vec<InspectedEntry> = oldest_first(inspected);
270 let mut needs: Vec<bool> = Vec::with_capacity(ordered.len());
271 for item in &ordered {
272 needs.push(needs_landing_lock(item)?);
273 }
274 let mut outcomes: Vec<RecoveryOutcome> = Vec::new();
275 if needs.contains(&true) {
276 let lock: OwnerLock = acquire_registry_lock(root, LANDING_LOCK_NAME)?;
277 let recovered: Result<Vec<RecoveryOutcome>, RecoveryError> =
278 recover_dead_landings(place, root);
279 let released: Result<(), super::owner_lock::OwnerLockError> = lock.release();
280 outcomes.extend(recovered?);
281 if let Err(error) = released {
282 return Err(RecoveryError(error.to_string()));
283 }
284 }
285 for (index, item) in ordered.iter().enumerate() {
286 if needs[index] || recovered_already(outcomes.as_slice(), item.entry.path.as_path()) {
287 continue;
288 }
289 let action: RecoveryAction = recover_inspected_entry(place, item)?;
290 outcomes.push(RecoveryOutcome {
291 directory: item.entry.path.clone(),
292 action,
293 });
294 }
295 for path in &retired {
296 if let Err(error) = super::private_storage::remove_tree(path.as_path()) {
297 return Err(io_failure(
298 "removing the retired transaction",
299 path.as_path(),
300 &error,
301 ));
302 }
303 }
304 if let Err(error) = retire_lock_of_dead_owner(root.join(RESERVATION_LOCK_NAME).as_path()) {
306 return Err(RecoveryError(error.to_string()));
307 }
308 for path in retired {
309 outcomes.push(RecoveryOutcome {
310 directory: path,
311 action: RecoveryAction::RetiredRemoved,
312 });
313 }
314 return Ok(outcomes);
315}
316
317#[cfg(test)]
319#[path = "recovery_scan_tests.rs"]
320mod tests;