Skip to content

Commit 257fe0b

Browse files
committed
Distinguish corrupt but existing leases
1 parent eaa09b9 commit 257fe0b

1 file changed

Lines changed: 112 additions & 26 deletions

File tree

src/lease.rs

Lines changed: 112 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
//! Leases controlling write access to an archive.
44
5+
use std::process;
56
use std::time::Duration;
67

78
use serde::{Deserialize, Serialize};
89
use thiserror::Error;
910
use time::OffsetDateTime;
10-
use tracing::{debug, instrument};
11+
use tracing::{debug, instrument, trace, warn};
1112
use url::Url;
1213

13-
use crate::jsonio::{self, read_json};
1414
use crate::transport::{self, Transport, WriteMode};
1515

1616
pub static LEASE_FILENAME: &str = "LEASE";
@@ -35,6 +35,9 @@ pub enum Error {
3535
content: Box<LeaseContent>,
3636
},
3737

38+
#[error("Existing lease file {url} is corrupt")]
39+
Corrupt { url: Url },
40+
3841
#[error("Transport error on lease file: {source}")]
3942
Transport {
4043
#[from]
@@ -50,7 +53,7 @@ type Result<T> = std::result::Result<T, Error>;
5053
impl Lease {
5154
/// Acquire a lease, if one is available.
5255
///
53-
/// Returns [Error::Busy] if the lease is already held by another process.
56+
/// Returns [Error::Busy] or [Error::Corrupt] if the lease is already held by another process.
5457
#[instrument]
5558
pub async fn acquire(transport: &Transport) -> Result<Self> {
5659
let lease_taken = OffsetDateTime::now_utc();
@@ -59,9 +62,10 @@ impl Lease {
5962
host: hostname::get()
6063
.unwrap_or_default()
6164
.to_string_lossy()
62-
.into_owned(),
63-
pid: std::process::id(),
64-
client_version: crate::VERSION.to_string(),
65+
.into_owned()
66+
.into(),
67+
pid: Some(process::id()),
68+
client_version: Some(crate::VERSION.to_string()),
6569
lease_taken,
6670
lease_expiry,
6771
};
@@ -74,14 +78,17 @@ impl Lease {
7478
{
7579
if err.kind() == transport::ErrorKind::AlreadyExists {
7680
match Lease::peek(transport).await? {
77-
Some(content) => {
81+
LeaseState::Held(content) => {
7882
return Err(Error::Busy {
7983
url,
8084
content: Box::new(content),
8185
})
8286
}
83-
None => {
84-
debug!("Lease file disappeared after conflict");
87+
LeaseState::Corrupt(_mtime) => {
88+
return Err(Error::Corrupt { url });
89+
}
90+
LeaseState::Free => {
91+
debug!("Lease file disappeared after conflict; retrying");
8592
continue;
8693
}
8794
}
@@ -108,28 +115,47 @@ impl Lease {
108115
}
109116

110117
/// Return information about the current leaseholder, if any.
111-
pub async fn peek(transport: &Transport) -> Result<Option<LeaseContent>> {
112-
read_json(transport, LEASE_FILENAME)
113-
.await
114-
.map_err(|err| match err {
115-
jsonio::Error::Transport { source, .. } => Error::Transport { source },
116-
jsonio::Error::Json { source, .. } => Error::Json {
117-
source,
118-
url: transport.url().join(LEASE_FILENAME).unwrap(),
119-
},
120-
})
118+
pub async fn peek(transport: &Transport) -> Result<LeaseState> {
119+
// TODO: Atomically get the content and mtime; that should be one call on s3.
120+
let metadata = match transport.metadata(LEASE_FILENAME).await {
121+
Ok(m) => m,
122+
Err(err) if err.is_not_found() => {
123+
trace!("lease file not present");
124+
return Ok(LeaseState::Free);
125+
}
126+
Err(err) => {
127+
warn!(?err, "error getting lease file metadata");
128+
return Err(err.into());
129+
}
130+
};
131+
let bytes = transport.read(LEASE_FILENAME).await?;
132+
match serde_json::from_slice(&bytes) {
133+
Ok(content) => Ok(LeaseState::Held(content)),
134+
Err(err) => {
135+
warn!(?err, "error deserializing lease file");
136+
// We do still at least know that it's held, and when it was taken.
137+
Ok(LeaseState::Corrupt(metadata.modified))
138+
}
139+
}
121140
}
122141
}
123142

143+
#[derive(Debug, Clone)]
144+
pub enum LeaseState {
145+
Free,
146+
Held(LeaseContent),
147+
Corrupt(OffsetDateTime),
148+
}
149+
124150
/// Contents of the lease file.
125-
#[derive(Debug, Serialize, Deserialize)]
151+
#[derive(Debug, Serialize, Deserialize, Clone)]
126152
pub struct LeaseContent {
127153
/// Hostname of the client process
128-
pub host: String,
154+
pub host: Option<String>,
129155
/// Process id of the client.
130-
pub pid: u32,
156+
pub pid: Option<u32>,
131157
/// Conserve version string.
132-
pub client_version: String,
158+
pub client_version: Option<String>,
133159

134160
/// Time when the lease was taken.
135161
#[serde(with = "time::serde::iso8601")]
@@ -142,6 +168,9 @@ pub struct LeaseContent {
142168

143169
#[cfg(test)]
144170
mod test {
171+
use std::fs::{write, File};
172+
use std::process;
173+
145174
use tempfile::TempDir;
146175

147176
use super::*;
@@ -154,11 +183,68 @@ mod test {
154183
assert!(tmp.path().join("LEASE").exists());
155184
assert!(lease.next_renewal > lease.lease_taken);
156185

157-
let peeked = Lease::peek(transport).await.unwrap().unwrap();
158-
assert_eq!(peeked.host, hostname::get().unwrap().to_string_lossy());
159-
assert_eq!(peeked.pid, std::process::id());
186+
let peeked = Lease::peek(transport).await.unwrap();
187+
let LeaseState::Held(content) = peeked else {
188+
panic!("lease not held")
189+
};
190+
assert_eq!(
191+
content.host.unwrap(),
192+
hostname::get().unwrap().to_string_lossy()
193+
);
194+
assert_eq!(content.pid, Some(process::id()));
160195

161196
lease.release().await.unwrap();
162197
assert!(!tmp.path().join("LEASE").exists());
163198
}
199+
200+
#[tokio::test]
201+
async fn peek_fixed_lease_content() {
202+
let tmp = TempDir::new().unwrap();
203+
let transport = &Transport::local(tmp.path());
204+
write(
205+
tmp.path().join("LEASE"),
206+
r#"
207+
{
208+
"host": "somehost",
209+
"pid": 1234,
210+
"client_version": "0.1.2",
211+
"lease_taken": "2021-01-01T12:34:56Z",
212+
"lease_expiry": "2021-01-01T12:35:56Z"
213+
}"#,
214+
)
215+
.unwrap();
216+
let state = Lease::peek(transport).await.unwrap();
217+
dbg!(&state);
218+
match state {
219+
LeaseState::Held(content) => {
220+
assert_eq!(content.host.unwrap(), "somehost");
221+
assert_eq!(content.pid, Some(1234));
222+
assert_eq!(content.client_version.unwrap(), "0.1.2");
223+
assert_eq!(content.lease_taken.year(), 2021);
224+
assert_eq!(content.lease_expiry.year(), 2021);
225+
assert_eq!(
226+
content.lease_expiry - content.lease_taken,
227+
time::Duration::seconds(60)
228+
);
229+
}
230+
_ => panic!("lease should be recognized as held, got {state:?}"),
231+
}
232+
}
233+
234+
/// An empty lease file is judged by its mtime; the lease can be grabbed a while
235+
/// after it was last written.
236+
#[tokio::test]
237+
async fn peek_corrupt_empty_lease() {
238+
let tmp = TempDir::new().unwrap();
239+
let transport = &Transport::local(tmp.path());
240+
File::create(tmp.path().join("LEASE")).unwrap();
241+
let state = Lease::peek(transport).await.unwrap();
242+
match state {
243+
LeaseState::Corrupt(mtime) => {
244+
let now = time::OffsetDateTime::now_utc();
245+
assert!(now - mtime < time::Duration::seconds(15));
246+
}
247+
_ => panic!("lease should be recognized as corrupt, got {state:?}"),
248+
}
249+
}
164250
}

0 commit comments

Comments
 (0)