1use std::io::{Result, Error, ErrorKind};
29use aws_sdk_s3::Client;
30use aws_sdk_s3::primitives::SdkBody;
31use crate::s3::s3_error_kind;
32use uuid::Uuid;
33use crate::hyper::HyperDir;
34use crate::layout::HyperDirLayout;
35use crate::unix_now_ms;
36
37const LOCK_OBJECT: &str = "_lock";
39pub const DEFAULT_LOCK_TTL_MS: u64 = 60_000;
41const LOCK_OCC_RETRIES: usize = 8;
43
44#[derive(Clone, Copy, PartialEq, Eq, Debug)]
47pub enum LockKind {
48 Read,
49 Write,
50 Unlock,
51}
52
53impl LockKind {
54 fn as_char(self) -> char {
55 match self {
56 LockKind::Read => 'R',
57 LockKind::Write => 'W',
58 LockKind::Unlock => 'U',
59 }
60 }
61 fn from_char(c: char) -> Option<Self> {
62 match c {
63 'R' => Some(LockKind::Read),
64 'W' => Some(LockKind::Write),
65 _ => None,
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct Conflict {
74 pub start: u64,
75 pub end: u64,
76 pub kind: LockKind,
77 pub pid: u32,
78}
79
80#[derive(Debug, PartialEq, Eq)]
82pub enum SetLkOutcome {
83 Granted,
84 Conflict(Conflict),
85}
86
87#[derive(Clone, Debug)]
88struct Entry {
89 owner: String,
90 kind: LockKind, start: u64,
92 end: u64, pid: u32,
94 expire_ms: i64,
95}
96
97#[derive(Default, Debug)]
99pub struct LockTable {
100 entries: Vec<Entry>,
101}
102
103fn overlaps(a_start: u64, a_end: u64, b_start: u64, b_end: u64) -> bool {
104 a_start <= b_end && b_start <= a_end
105}
106
107impl LockTable {
108 fn purge_expired(&mut self, now_ms: i64) {
109 self.entries.retain(|e| e.expire_ms > now_ms);
110 }
111
112 pub fn test(&self, owner: &str, kind: LockKind, start: u64, end: u64, now_ms: i64) -> Option<Conflict> {
115 let want_write = kind == LockKind::Write;
116 for e in &self.entries {
117 if e.expire_ms <= now_ms || e.owner == owner {
118 continue;
119 }
120 if overlaps(start, end, e.start, e.end) && (want_write || e.kind == LockKind::Write) {
121 return Some(Conflict { start: e.start, end: e.end, kind: e.kind, pid: e.pid });
122 }
123 }
124 None
125 }
126
127 fn remove_range(&mut self, owner: &str, start: u64, end: u64) {
129 let mut out = Vec::with_capacity(self.entries.len());
130 for e in std::mem::take(&mut self.entries) {
131 if e.owner != owner || !overlaps(start, end, e.start, e.end) {
132 out.push(e);
133 continue;
134 }
135 if e.start < start {
136 out.push(Entry { end: start - 1, ..e.clone() }); }
138 if e.end > end {
139 out.push(Entry { start: end + 1, ..e.clone() }); }
141 }
143 self.entries = out;
144 }
145
146 #[allow(clippy::too_many_arguments)]
151 pub fn set(&mut self, owner: &str, kind: LockKind, start: u64, end: u64, pid: u32, expire_ms: i64, now_ms: i64)
152 -> std::result::Result<(), Conflict>
153 {
154 self.purge_expired(now_ms);
155 if kind == LockKind::Unlock {
156 self.remove_range(owner, start, end);
157 return Ok(());
158 }
159 if let Some(c) = self.test(owner, kind, start, end, now_ms) {
160 return Err(c);
161 }
162 self.remove_range(owner, start, end);
163 self.entries.push(Entry { owner: owner.to_string(), kind, start, end, pid, expire_ms });
164 Ok(())
165 }
166
167 pub fn release_owner(&mut self, owner: &str) -> bool {
170 let before = self.entries.len();
171 self.entries.retain(|e| e.owner != owner);
172 self.entries.len() != before
173 }
174
175 pub fn renew(&mut self, owner: &str, expire_ms: i64) -> bool {
178 let mut any = false;
179 for e in &mut self.entries {
180 if e.owner == owner {
181 e.expire_ms = expire_ms;
182 any = true;
183 }
184 }
185 any
186 }
187
188 pub fn encode(&self) -> Vec<u8> {
191 let mut s = String::from("v1\n");
192 for e in &self.entries {
193 s.push_str(&format!(
194 "{}\t{}\t{}\t{}\t{}\t{}\n",
195 e.owner, e.kind.as_char(), e.start, e.end, e.pid, e.expire_ms
196 ));
197 }
198 s.into_bytes()
199 }
200
201 pub fn decode(buf: &[u8]) -> Self {
205 let mut t = LockTable::default();
206 let Ok(s) = std::str::from_utf8(buf) else { return t; };
207 for line in s.lines().skip(1) {
208 let f: Vec<&str> = line.split('\t').collect();
210 if f.len() != 6 {
211 continue;
212 }
213 let (Some(kind), Ok(start), Ok(end), Ok(pid), Ok(expire_ms)) = (
214 f[1].chars().next().and_then(LockKind::from_char),
215 f[2].parse::<u64>(),
216 f[3].parse::<u64>(),
217 f[4].parse::<u32>(),
218 f[5].parse::<i64>(),
219 ) else {
220 continue;
221 };
222 t.entries.push(Entry { owner: f[0].to_string(), kind, start, end, pid, expire_ms });
223 }
224 t
225 }
226}
227
228fn lock_key(layout: &HyperDirLayout, uuid: &Uuid, is_dir: bool) -> String {
230 let base = if is_dir { layout.dir_key(uuid) } else { layout.file_key(uuid) };
231 format!("{}/{}", base, LOCK_OBJECT)
232}
233
234async fn load_table(client: &Client, bucket: &str, key: &str) -> Result<(LockTable, Option<String>)> {
237 match client.get_object().bucket(bucket).key(key).send().await {
238 Ok(res) => {
239 let etag = res.e_tag.clone().map(|t| t.replace('"', ""));
240 let bytes = res.body.collect().await
241 .map_err(|e| Error::other(format!("collect _lock s3://{}/{}: {}", bucket, key, e)))?
242 .into_bytes();
243 Ok((LockTable::decode(&bytes), etag))
244 },
245 Err(e) if e.as_service_error().is_some_and(|s| s.is_no_such_key()) => Ok((LockTable::default(), None)),
246 Err(e) => Err(Error::new(s3_error_kind(&e), format!("GetObject _lock s3://{}/{}: {}", bucket, key, e))),
247 }
248}
249
250enum PutOutcome {
251 Ok,
252 Retry, }
254
255async fn cas_put(client: &Client, bucket: &str, key: &str, body: Vec<u8>, etag: Option<&str>) -> Result<PutOutcome> {
258 let b = client.put_object().bucket(bucket).key(key).body(SdkBody::from(body).into());
259 let b = match etag {
260 Some(t) => b.if_match(t),
261 None => b.if_none_match('*'),
262 };
263 match b.send().await {
264 Ok(_) => Ok(PutOutcome::Ok),
265 Err(sdk_err) => {
266 if sdk_err.raw_response().map(|r| matches!(r.status().as_u16(), 412 | 409)).unwrap_or(false) {
267 Ok(PutOutcome::Retry)
268 } else {
269 Err(Error::new(s3_error_kind(&sdk_err), format!("PutObject _lock s3://{}/{}: {}", bucket, key, sdk_err)))
270 }
271 },
272 }
273}
274
275impl HyperDir<'_> {
276 #[allow(clippy::too_many_arguments)]
278 pub async fn fs_getlk(
279 client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid, is_dir: bool,
280 owner: &str, kind: LockKind, start: u64, end: u64,
281 ) -> Result<Option<Conflict>> {
282 let key = lock_key(layout, uuid, is_dir);
283 let (table, _) = load_table(client, bucket, &key).await?;
284 Ok(table.test(owner, kind, start, end, unix_now_ms()))
285 }
286
287 #[allow(clippy::too_many_arguments)]
290 pub async fn fs_setlk(
291 client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid, is_dir: bool,
292 owner: &str, kind: LockKind, start: u64, end: u64, pid: u32, ttl_ms: u64,
293 ) -> Result<SetLkOutcome> {
294 let key = lock_key(layout, uuid, is_dir);
295 for _ in 0..LOCK_OCC_RETRIES {
296 let (mut table, etag) = load_table(client, bucket, &key).await?;
297 if kind == LockKind::Unlock && etag.is_none() {
299 return Ok(SetLkOutcome::Granted);
300 }
301 let now = unix_now_ms();
302 let expire = now.saturating_add(ttl_ms as i64);
303 if let Err(c) = table.set(owner, kind, start, end, pid, expire, now) {
304 return Ok(SetLkOutcome::Conflict(c));
305 }
306 match cas_put(client, bucket, &key, table.encode(), etag.as_deref()).await? {
307 PutOutcome::Ok => return Ok(SetLkOutcome::Granted),
308 PutOutcome::Retry => continue,
309 }
310 }
311 Err(Error::new(ErrorKind::ResourceBusy, "fs_setlk: too many OCC conflicts"))
312 }
313
314 pub async fn fs_unlock_owner(
316 client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid, is_dir: bool, owner: &str,
317 ) -> Result<()> {
318 let key = lock_key(layout, uuid, is_dir);
319 for _ in 0..LOCK_OCC_RETRIES {
320 let (mut table, etag) = load_table(client, bucket, &key).await?;
321 let Some(etag) = etag else { return Ok(()); }; if !table.release_owner(owner) {
323 return Ok(()); }
325 match cas_put(client, bucket, &key, table.encode(), Some(&etag)).await? {
326 PutOutcome::Ok => return Ok(()),
327 PutOutcome::Retry => continue,
328 }
329 }
330 Err(Error::new(ErrorKind::ResourceBusy, "fs_unlock_owner: too many OCC conflicts"))
331 }
332
333 pub async fn fs_lock_renew(
336 client: &Client, layout: &HyperDirLayout, bucket: &str, uuid: &Uuid, is_dir: bool,
337 owner: &str, ttl_ms: u64,
338 ) -> Result<()> {
339 let key = lock_key(layout, uuid, is_dir);
340 for _ in 0..LOCK_OCC_RETRIES {
341 let (mut table, etag) = load_table(client, bucket, &key).await?;
342 let Some(etag) = etag else { return Ok(()); };
343 let expire = unix_now_ms().saturating_add(ttl_ms as i64);
344 if !table.renew(owner, expire) {
345 return Ok(());
346 }
347 match cas_put(client, bucket, &key, table.encode(), Some(&etag)).await? {
348 PutOutcome::Ok => return Ok(()),
349 PutOutcome::Retry => continue,
350 }
351 }
352 Err(Error::new(ErrorKind::ResourceBusy, "fs_lock_renew: too many OCC conflicts"))
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 const FUTURE: i64 = i64::MAX;
361 const NOW: i64 = 1_000;
362
363 fn set(t: &mut LockTable, owner: &str, kind: LockKind, s: u64, e: u64) -> std::result::Result<(), Conflict> {
364 t.set(owner, kind, s, e, 0, FUTURE, NOW)
365 }
366
367 #[test]
368 fn read_read_compatible_write_excludes() {
369 let mut t = LockTable::default();
370 set(&mut t, "a", LockKind::Read, 0, 10).unwrap();
371 set(&mut t, "b", LockKind::Read, 5, 20).unwrap();
373 let c = set(&mut t, "c", LockKind::Write, 8, 9).unwrap_err();
375 assert_eq!(c.kind, LockKind::Read);
376 set(&mut t, "c", LockKind::Write, 100, 200).unwrap();
378 }
379
380 #[test]
381 fn write_write_overlap_conflicts_disjoint_ok() {
382 let mut t = LockTable::default();
383 set(&mut t, "a", LockKind::Write, 0, 10).unwrap();
384 assert!(set(&mut t, "b", LockKind::Write, 10, 10).is_err()); set(&mut t, "b", LockKind::Write, 11, 20).unwrap(); }
387
388 #[test]
389 fn same_owner_replaces_and_upgrades() {
390 let mut t = LockTable::default();
391 set(&mut t, "a", LockKind::Read, 0, 10).unwrap();
392 set(&mut t, "a", LockKind::Write, 0, 10).unwrap();
394 assert!(set(&mut t, "b", LockKind::Write, 0, 0).is_err());
396 }
397
398 #[test]
399 fn unlock_splits_middle() {
400 let mut t = LockTable::default();
401 set(&mut t, "a", LockKind::Write, 0, 100).unwrap();
402 set(&mut t, "a", LockKind::Unlock, 40, 60).unwrap();
404 assert!(set(&mut t, "b", LockKind::Write, 50, 50).is_ok());
406 assert!(set(&mut t, "c", LockKind::Write, 0, 0).is_err());
407 assert!(set(&mut t, "d", LockKind::Write, 100, 100).is_err());
408 }
409
410 #[test]
411 fn release_owner_and_renew() {
412 let mut t = LockTable::default();
413 set(&mut t, "a", LockKind::Write, 0, 10).unwrap();
414 set(&mut t, "a", LockKind::Write, 20, 30).unwrap();
415 assert!(t.renew("a", FUTURE));
416 assert!(!t.renew("nobody", FUTURE));
417 assert!(t.release_owner("a"));
418 assert!(t.entries.is_empty());
419 assert!(set(&mut t, "b", LockKind::Write, 0, 30).is_ok());
420 }
421
422 #[test]
423 fn expired_locks_are_ignored() {
424 let mut t = LockTable::default();
425 t.set("a", LockKind::Write, 0, 10, 0, NOW - 1, NOW).unwrap();
427 t.set("b", LockKind::Write, 0, 10, 0, FUTURE, NOW).unwrap();
429 assert!(t.test("c", LockKind::Read, 5, 5, NOW).is_some());
430 }
431
432 #[test]
433 fn encode_decode_roundtrip() {
434 let mut t = LockTable::default();
435 set(&mut t, "client-1:42", LockKind::Read, 0, 10).unwrap();
436 set(&mut t, "client-2:7", LockKind::Write, 100, u64::MAX).unwrap();
437 let back = LockTable::decode(&t.encode());
438 assert!(back.test("x", LockKind::Read, 0, 10, NOW).is_none());
440 assert!(back.test("x", LockKind::Write, 0, 0, NOW).is_some());
441 assert!(back.test("x", LockKind::Read, 200, 200, NOW).is_some());
442 }
443
444 #[test]
445 fn decode_tolerates_garbage() {
446 assert!(LockTable::decode(b"").entries.is_empty());
447 assert!(LockTable::decode(b"v1\ngarbage line\n\tbad\t\t\t\t").entries.is_empty());
448 }
449}