]> git.proxmox.com Git - rustc.git/blame - vendor/gix-lock/src/acquire.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / vendor / gix-lock / src / acquire.rs
CommitLineData
0a29b90c
FG
1use std::{
2 fmt,
3 path::{Path, PathBuf},
4 time::Duration,
5};
6
7use gix_tempfile::{AutoRemove, ContainingDirectory};
8
9use crate::{backoff, File, Marker, DOT_LOCK_SUFFIX};
10
11/// Describe what to do if a lock cannot be obtained as it's already held elsewhere.
fe692bf9 12#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
0a29b90c
FG
13pub enum Fail {
14 /// Fail after the first unsuccessful attempt of obtaining a lock.
fe692bf9 15 #[default]
0a29b90c
FG
16 Immediately,
17 /// Retry after failure with exponentially longer sleep times to block the current thread.
18 /// Fail once the given duration is exceeded, similar to [Fail::Immediately]
19 AfterDurationWithBackoff(Duration),
20}
21
0a29b90c
FG
22impl fmt::Display for Fail {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Fail::Immediately => f.write_str("immediately"),
26 Fail::AfterDurationWithBackoff(duration) => {
27 write!(f, "after {:.02}s", duration.as_secs_f32())
28 }
29 }
30 }
31}
32
781aab86
FG
33impl From<Duration> for Fail {
34 fn from(value: Duration) -> Self {
35 if value.is_zero() {
36 Fail::Immediately
37 } else {
38 Fail::AfterDurationWithBackoff(value)
39 }
40 }
41}
42
0a29b90c
FG
43/// The error returned when acquiring a [`File`] or [`Marker`].
44#[derive(Debug, thiserror::Error)]
45#[allow(missing_docs)]
46pub enum Error {
47 #[error("Another IO error occurred while obtaining the lock")]
48 Io(#[from] std::io::Error),
49 #[error("The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.", super::DOT_LOCK_SUFFIX)]
50 PermanentlyLocked {
51 resource_path: PathBuf,
52 mode: Fail,
53 attempts: usize,
54 },
55}
56
57impl File {
58 /// Create a writable lock file with failure `mode` whose content will eventually overwrite the given resource `at_path`.
59 ///
60 /// If `boundary_directory` is given, non-existing directories will be created automatically and removed in the case of
61 /// a rollback. Otherwise the containing directory is expected to exist, even though the resource doesn't have to.
781aab86
FG
62 ///
63 /// ### Warning of potential resource leak
64 ///
65 /// Please note that the underlying file will remain if destructors don't run, as is the case when interrupting the application.
66 /// This results in the resource being locked permanently unless the lock file is removed by other means.
67 /// See [the crate documentation](crate) for more information.
0a29b90c
FG
68 pub fn acquire_to_update_resource(
69 at_path: impl AsRef<Path>,
70 mode: Fail,
71 boundary_directory: Option<PathBuf>,
72 ) -> Result<File, Error> {
781aab86 73 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
0a29b90c
FG
74 gix_tempfile::writable_at(p, d, c)
75 })?;
76 Ok(File {
77 inner: handle,
78 lock_path,
79 })
80 }
81}
82
83impl Marker {
84 /// Like [`acquire_to_update_resource()`][File::acquire_to_update_resource()] but _without_ the possibility to make changes
85 /// and commit them.
86 ///
87 /// If `boundary_directory` is given, non-existing directories will be created automatically and removed in the case of
88 /// a rollback.
781aab86
FG
89 ///
90 /// ### Warning of potential resource leak
91 ///
92 /// Please note that the underlying file will remain if destructors don't run, as is the case when interrupting the application.
93 /// This results in the resource being locked permanently unless the lock file is removed by other means.
94 /// See [the crate documentation](crate) for more information.
0a29b90c
FG
95 pub fn acquire_to_hold_resource(
96 at_path: impl AsRef<Path>,
97 mode: Fail,
98 boundary_directory: Option<PathBuf>,
99 ) -> Result<Marker, Error> {
781aab86 100 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
0a29b90c
FG
101 gix_tempfile::mark_at(p, d, c)
102 })?;
103 Ok(Marker {
104 created_from_file: false,
105 inner: handle,
106 lock_path,
107 })
108 }
109}
110
111fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
112 match boundary {
113 None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
114 Some(boundary_directory) => (
115 ContainingDirectory::CreateAllRaceProof(Default::default()),
116 AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
117 ),
118 }
119}
120
121fn lock_with_mode<T>(
122 resource: &Path,
123 mode: Fail,
124 boundary_directory: Option<PathBuf>,
781aab86 125 try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
0a29b90c
FG
126) -> Result<(PathBuf, T), Error> {
127 use std::io::ErrorKind::*;
128 let (directory, cleanup) = dir_cleanup(boundary_directory);
129 let lock_path = add_lock_suffix(resource);
130 let mut attempts = 1;
131 match mode {
132 Fail::Immediately => try_lock(&lock_path, directory, cleanup),
133 Fail::AfterDurationWithBackoff(time) => {
134 for wait in backoff::Exponential::default_with_random().until_no_remaining(time) {
135 attempts += 1;
136 match try_lock(&lock_path, directory, cleanup.clone()) {
137 Ok(v) => return Ok((lock_path, v)),
138 #[cfg(windows)]
139 Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
140 std::thread::sleep(wait);
141 continue;
142 }
143 #[cfg(not(windows))]
144 Err(err) if err.kind() == AlreadyExists => {
145 std::thread::sleep(wait);
146 continue;
147 }
148 Err(err) => return Err(Error::from(err)),
149 }
150 }
151 try_lock(&lock_path, directory, cleanup)
152 }
153 }
154 .map(|v| (lock_path, v))
155 .map_err(|err| match err.kind() {
156 AlreadyExists => Error::PermanentlyLocked {
157 resource_path: resource.into(),
158 mode,
159 attempts,
160 },
161 _ => Error::Io(err),
162 })
163}
164
165fn add_lock_suffix(resource_path: &Path) -> PathBuf {
166 resource_path.with_extension(resource_path.extension().map_or_else(
167 || DOT_LOCK_SUFFIX.chars().skip(1).collect(),
168 |ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
169 ))
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn add_lock_suffix_to_file_with_extension() {
178 assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
179 }
180
181 #[test]
182 fn add_lock_suffix_to_file_without_extension() {
183 assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
184 }
185}