]>
Commit | Line | Data |
---|---|---|
0a29b90c FG |
1 | use std::{ |
2 | fmt, | |
3 | path::{Path, PathBuf}, | |
4 | time::Duration, | |
5 | }; | |
6 | ||
7 | use gix_tempfile::{AutoRemove, ContainingDirectory}; | |
8 | ||
9 | use 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 |
13 | pub 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 |
22 | impl 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 |
33 | impl 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)] | |
46 | pub 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 | ||
57 | impl 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 | ||
83 | impl 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 | ||
111 | fn 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 | ||
121 | fn 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 | ||
165 | fn 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)] | |
173 | mod 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 | } |