corium_transactor/
lease.rs1use corium_store::{RootStore, StoreError};
12use thiserror::Error;
13
14#[must_use]
16pub fn lease_root(db: &str) -> String {
17 format!("lease:{db}")
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct Lease {
23 pub owner: String,
25 pub version: u64,
27 pub expires_unix_ms: i64,
29}
30
31impl Lease {
32 #[must_use]
34 pub fn encode(&self) -> Vec<u8> {
35 format!(
36 "{}\n{}\n{}\n",
37 self.owner, self.version, self.expires_unix_ms
38 )
39 .into_bytes()
40 }
41
42 #[must_use]
44 pub fn decode(bytes: &[u8]) -> Option<Self> {
45 let text = std::str::from_utf8(bytes).ok()?;
46 let mut lines = text.lines();
47 Some(Self {
48 owner: lines.next()?.to_owned(),
49 version: lines.next()?.parse().ok()?,
50 expires_unix_ms: lines.next()?.parse().ok()?,
51 })
52 }
53}
54
55#[derive(Debug, Error)]
57pub enum LeaseError {
58 #[error("lease held by {owner} until {expires_unix_ms}")]
60 Held {
61 owner: String,
63 expires_unix_ms: i64,
65 },
66 #[error("lease lost to another owner")]
68 Lost,
69 #[error(transparent)]
71 Store(#[from] StoreError),
72}
73
74pub fn acquire(
82 store: &dyn RootStore,
83 db: &str,
84 owner: &str,
85 ttl_ms: i64,
86 now_unix_ms: i64,
87) -> Result<Lease, LeaseError> {
88 let name = lease_root(db);
89 loop {
90 let current = store.get_root(&name)?;
91 let decoded = current.as_deref().and_then(Lease::decode);
92 let version = match &decoded {
93 Some(lease) if lease.owner == owner => lease.version,
94 Some(lease) if lease.expires_unix_ms > now_unix_ms => {
95 return Err(LeaseError::Held {
96 owner: lease.owner.clone(),
97 expires_unix_ms: lease.expires_unix_ms,
98 });
99 }
100 Some(lease) => lease.version + 1,
101 None => 1,
102 };
103 let next = Lease {
104 owner: owner.to_owned(),
105 version,
106 expires_unix_ms: now_unix_ms + ttl_ms,
107 };
108 match store.cas_root(&name, current.as_deref(), &next.encode()) {
109 Ok(()) => return Ok(next),
110 Err(StoreError::CasFailed { .. }) => {}
111 Err(error) => return Err(error.into()),
112 }
113 }
114}
115
116pub fn renew(
122 store: &dyn RootStore,
123 db: &str,
124 held: &Lease,
125 ttl_ms: i64,
126 now_unix_ms: i64,
127) -> Result<Lease, LeaseError> {
128 let name = lease_root(db);
129 let current = store.get_root(&name)?;
130 if current.as_deref() != Some(held.encode().as_slice()) {
131 return Err(LeaseError::Lost);
132 }
133 let next = Lease {
134 owner: held.owner.clone(),
135 version: held.version,
136 expires_unix_ms: now_unix_ms + ttl_ms,
137 };
138 match store.cas_root(&name, current.as_deref(), &next.encode()) {
139 Ok(()) => Ok(next),
140 Err(StoreError::CasFailed { .. }) => Err(LeaseError::Lost),
141 Err(error) => Err(error.into()),
142 }
143}
144
145pub fn release(store: &dyn RootStore, db: &str, held: &Lease) -> Result<(), LeaseError> {
151 let name = lease_root(db);
152 let current = store.get_root(&name)?;
153 if current.as_deref() != Some(held.encode().as_slice()) {
154 return Err(LeaseError::Lost);
155 }
156 let expired = Lease {
157 expires_unix_ms: 0,
158 ..held.clone()
159 };
160 match store.cas_root(&name, current.as_deref(), &expired.encode()) {
161 Ok(()) => Ok(()),
162 Err(StoreError::CasFailed { .. }) => Err(LeaseError::Lost),
163 Err(error) => Err(error.into()),
164 }
165}