22
33//! Leases controlling write access to an archive.
44
5+ use std:: process;
56use std:: time:: Duration ;
67
78use serde:: { Deserialize , Serialize } ;
89use thiserror:: Error ;
910use time:: OffsetDateTime ;
10- use tracing:: { debug, instrument} ;
11+ use tracing:: { debug, instrument, trace , warn } ;
1112use url:: Url ;
1213
13- use crate :: jsonio:: { self , read_json} ;
1414use crate :: transport:: { self , Transport , WriteMode } ;
1515
1616pub 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>;
5053impl 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 ) ]
126152pub 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) ]
144170mod 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