1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use time::OffsetDateTime;
7use time::format_description::well_known::Rfc3339;
8use tokio::fs;
9use tokio::io::AsyncWriteExt;
10
11use crate::error::Error;
12use crate::namespace::Namespace;
13use crate::storage::s3::Keyspace;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Lock {
17 pub id: String,
18 pub path: String,
19 pub locked_at: String,
20 pub owner: Owner,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Owner {
25 pub name: String,
26}
27
28pub struct LockStore {
35 backend: Backend,
36 max_age: Option<Duration>,
37 conditional_writes: bool,
43}
44
45enum Backend {
46 Local { root: PathBuf },
47 Bucket(Box<Keyspace>),
48}
49
50impl LockStore {
51 pub fn local(root: impl Into<PathBuf>) -> Self {
52 Self::over(Backend::Local { root: root.into() })
53 }
54
55 pub fn bucket(keys: Keyspace) -> Self {
56 Self::over(Backend::Bucket(Box::new(keys)))
57 }
58
59 fn over(backend: Backend) -> Self {
60 Self {
61 backend,
62 max_age: None,
63 conditional_writes: true,
64 }
65 }
66
67 pub fn with_max_age(mut self, max_age: Option<Duration>) -> Self {
68 self.max_age = max_age;
69 self
70 }
71
72 pub fn with_conditional_writes(mut self, supported: bool) -> Self {
76 self.conditional_writes = supported;
77 self
78 }
79
80 pub fn max_age(&self) -> Option<Duration> {
81 self.max_age
82 }
83
84 pub fn stale_for(&self, lock: &Lock) -> Option<Duration> {
87 stale_for(lock, self.max_age)
88 }
89
90 pub fn id_of(path: &str) -> String {
91 hex::encode(Sha256::digest(path.as_bytes()))[..32].to_owned()
92 }
93
94 fn prefix(ns: &Namespace) -> String {
97 format!(".locks/{}/{}/", ns.org(), ns.repo())
98 }
99
100 fn key_of(ns: &Namespace, id: &str) -> String {
101 format!("{}{id}.json", Self::prefix(ns))
102 }
103
104 pub async fn create(&self, ns: &Namespace, path: &str, owner: &str) -> Result<Lock, Error> {
105 if path.is_empty() {
106 return Err(Error::MalformedLockPath);
107 }
108
109 let lock = Lock {
110 id: Self::id_of(path),
111 path: path.to_owned(),
112 locked_at: OffsetDateTime::now_utc()
113 .format(&Rfc3339)
114 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_owned()),
115 owner: Owner {
116 name: owner.to_owned(),
117 },
118 };
119 let encoded = serde_json::to_vec(&lock)?;
120
121 if self.take(ns, &lock, &encoded).await? {
122 return Ok(lock);
123 }
124
125 let Some(held) = self.get(ns, &lock.id).await? else {
130 return Err(Error::LockHeld(Box::new(lock)));
131 };
132
133 let Some(age) = self.stale_for(&held) else {
134 return Err(Error::LockHeld(Box::new(held)));
135 };
136
137 self.discard(ns, &held.id).await?;
142
143 if !self.take(ns, &lock, &encoded).await? {
144 return match self.get(ns, &lock.id).await? {
145 Some(other) => Err(Error::LockHeld(Box::new(other))),
146 None => Err(Error::LockHeld(Box::new(lock))),
147 };
148 }
149
150 tracing::info!(
151 path = lock.path,
152 previous_owner = held.owner.name,
153 untouched_for_seconds = age.as_secs(),
154 new_owner = lock.owner.name,
155 "a lock nobody had touched was taken over"
156 );
157
158 Ok(lock)
159 }
160
161 async fn take(&self, ns: &Namespace, lock: &Lock, encoded: &[u8]) -> Result<bool, Error> {
162 match &self.backend {
163 Backend::Local { root } => {
164 Self::write_new(&Self::path_in(root, ns, &lock.id), encoded).await
165 }
166 Backend::Bucket(_) if !self.conditional_writes => Err(Error::Unsupported(
171 "this object store does not refuse a conditional write, so a lock here could be \
172 held by two people at once",
173 )),
174 Backend::Bucket(bucket) => {
175 bucket
176 .put_if_absent(&Self::key_of(ns, &lock.id), encoded.to_vec())
177 .await
178 }
179 }
180 }
181
182 async fn discard(&self, ns: &Namespace, id: &str) -> Result<(), Error> {
185 match self.remove(ns, id).await {
186 Ok(()) | Err(Error::LockNotFound) => Ok(()),
187 Err(error) => Err(error),
188 }
189 }
190
191 async fn write_new(path: &Path, encoded: &[u8]) -> Result<bool, Error> {
192 let parent = path.parent().expect("lock paths have a parent");
193 fs::create_dir_all(parent).await?;
194
195 match fs::OpenOptions::new()
196 .write(true)
197 .create_new(true)
198 .open(path)
199 .await
200 {
201 Ok(mut file) => {
202 file.write_all(encoded).await?;
203 file.sync_all().await?;
204 Ok(true)
205 }
206 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
207 Err(error) => Err(error.into()),
208 }
209 }
210
211 pub async fn get(&self, ns: &Namespace, id: &str) -> Result<Option<Lock>, Error> {
212 if !is_well_formed_id(id) {
213 return Ok(None);
214 }
215
216 let encoded = match &self.backend {
217 Backend::Local { root } => match fs::read(Self::path_in(root, ns, id)).await {
218 Ok(bytes) => Some(bytes),
219 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
220 Err(error) => return Err(error.into()),
221 },
222 Backend::Bucket(bucket) => bucket.get_bytes(&Self::key_of(ns, id)).await?,
223 };
224
225 Ok(encoded.and_then(|bytes| serde_json::from_slice(&bytes).ok()))
226 }
227
228 pub async fn list(&self, ns: &Namespace) -> Result<Vec<Lock>, Error> {
229 let mut locks = match &self.backend {
230 Backend::Local { root } => Self::list_local(&Self::directory_in(root, ns)).await?,
231 Backend::Bucket(bucket) => Self::list_bucket(bucket, ns).await?,
232 };
233
234 locks.sort_by(|a: &Lock, b: &Lock| a.path.cmp(&b.path));
235 Ok(locks)
236 }
237
238 async fn list_local(directory: &Path) -> Result<Vec<Lock>, Error> {
239 let Ok(mut entries) = fs::read_dir(directory).await else {
240 return Ok(Vec::new());
241 };
242
243 let mut locks = Vec::new();
244 while let Some(entry) = entries.next_entry().await? {
245 if let Ok(bytes) = fs::read(entry.path()).await
246 && let Ok(lock) = serde_json::from_slice(&bytes)
247 {
248 locks.push(lock);
249 }
250 }
251
252 Ok(locks)
253 }
254
255 async fn list_bucket(bucket: &Keyspace, ns: &Namespace) -> Result<Vec<Lock>, Error> {
260 let mut locks = Vec::new();
261
262 for key in bucket.keys(&Self::prefix(ns)).await? {
263 if let Some(bytes) = bucket.get_bytes(&key).await?
264 && let Ok(lock) = serde_json::from_slice(&bytes)
265 {
266 locks.push(lock);
267 }
268 }
269
270 Ok(locks)
271 }
272
273 pub async fn remove(&self, ns: &Namespace, id: &str) -> Result<(), Error> {
274 if !is_well_formed_id(id) {
275 return Err(Error::LockNotFound);
276 }
277
278 let removed = match &self.backend {
279 Backend::Local { root } => match fs::remove_file(Self::path_in(root, ns, id)).await {
280 Ok(()) => true,
281 Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
282 Err(error) => return Err(error.into()),
283 },
284 Backend::Bucket(bucket) => bucket.delete(&Self::key_of(ns, id)).await?,
285 };
286
287 removed.then_some(()).ok_or(Error::LockNotFound)
288 }
289
290 fn directory_in(root: &Path, ns: &Namespace) -> PathBuf {
291 root.join(".locks").join(ns.org()).join(ns.repo())
292 }
293
294 fn path_in(root: &Path, ns: &Namespace, id: &str) -> PathBuf {
295 Self::directory_in(root, ns).join(format!("{id}.json"))
296 }
297}
298
299pub fn stale_for(lock: &Lock, max_age: Option<Duration>) -> Option<Duration> {
303 let max_age = max_age?;
304 let taken = OffsetDateTime::parse(&lock.locked_at, &Rfc3339).ok()?;
305
306 let age = Duration::try_from(OffsetDateTime::now_utc() - taken).ok()?;
308
309 (age > max_age).then_some(age)
310}
311
312fn is_well_formed_id(id: &str) -> bool {
313 id.len() == 32 && id.bytes().all(|b| b.is_ascii_hexdigit())
314}
315
316#[cfg(test)]
317mod tests;