1use corium_store::{DbRoot, FORMAT_VERSION, RootStore, StoreError, db_root_name};
18use thiserror::Error;
19
20#[must_use]
22pub fn lease_root(db: &str) -> String {
23 format!("lease:{db}")
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct Lease {
29 pub owner: String,
31 pub version: u64,
33 pub expires_unix_ms: i64,
35 pub endpoint: String,
37}
38
39#[derive(Debug, Error)]
41pub enum LeaseError {
42 #[error("lease held by {owner} until {expires_unix_ms}")]
44 Held {
45 owner: String,
47 expires_unix_ms: i64,
49 },
50 #[error("lease lost to another owner")]
52 Lost,
53 #[error(transparent)]
55 Store(#[from] StoreError),
56}
57
58fn decode_legacy(bytes: &[u8]) -> Option<(String, u64, i64)> {
60 let text = std::str::from_utf8(bytes).ok()?;
61 let mut lines = text.lines();
62 Some((
63 lines.next()?.to_owned(),
64 lines.next()?.parse().ok()?,
65 lines.next()?.parse().ok()?,
66 ))
67}
68
69fn holder(root: Option<&DbRoot>, legacy: Option<&(String, u64, i64)>) -> (String, u64, i64) {
71 match root {
72 Some(root) if !root.owner.is_empty() => (
73 root.owner.clone(),
74 root.lease_version,
75 root.lease_expires_unix_ms,
76 ),
77 Some(root) => match legacy {
78 Some((owner, version, expiry)) => {
79 (owner.clone(), (*version).max(root.lease_version), *expiry)
80 }
81 None => (String::new(), root.lease_version, 0),
82 },
83 None => match legacy {
84 Some((owner, version, expiry)) => (owner.clone(), *version, *expiry),
85 None => (String::new(), 0, 0),
86 },
87 }
88}
89
90pub async fn acquire(
101 store: &dyn RootStore,
102 db: &str,
103 owner: &str,
104 endpoint: &str,
105 ttl_ms: i64,
106 now_unix_ms: i64,
107) -> Result<Lease, LeaseError> {
108 let name = db_root_name(db);
109 let legacy_name = lease_root(db);
110 loop {
111 let current_bytes = store.get_root(&name).await?;
112 let current = current_bytes.as_deref().and_then(DbRoot::decode);
113 let legacy = store
114 .get_root(&legacy_name)
115 .await?
116 .as_deref()
117 .and_then(decode_legacy);
118 let (cur_owner, cur_version, cur_expiry) = holder(current.as_ref(), legacy.as_ref());
119 let version = if !cur_owner.is_empty() && cur_owner == owner {
120 cur_version
121 } else if cur_expiry > now_unix_ms {
122 return Err(LeaseError::Held {
123 owner: cur_owner,
124 expires_unix_ms: cur_expiry,
125 });
126 } else {
127 cur_version + 1
128 };
129 let next = DbRoot {
130 format_version: FORMAT_VERSION,
131 lease_version: version,
132 owner: owner.to_owned(),
133 lease_expires_unix_ms: now_unix_ms + ttl_ms,
134 owner_endpoint: endpoint.to_owned(),
135 index_basis_t: current.as_ref().map_or(0, |root| root.index_basis_t),
136 roots: current.as_ref().and_then(|root| root.roots.clone()),
137 next_entity_id: current.as_ref().map_or(0, |root| root.next_entity_id),
140 last_tx_instant: current
141 .as_ref()
142 .map_or(i64::MIN, |root| root.last_tx_instant),
143 key_manifest_version: current.as_ref().map_or(0, |root| root.key_manifest_version),
146 };
147 match store
148 .cas_root(&name, current_bytes.as_deref(), &next.encode())
149 .await
150 {
151 Ok(()) => {
152 if legacy.is_some() {
153 let _ = store.delete_root(&legacy_name).await;
155 }
156 return Ok(Lease {
157 owner: owner.to_owned(),
158 version,
159 expires_unix_ms: next.lease_expires_unix_ms,
160 endpoint: endpoint.to_owned(),
161 });
162 }
163 Err(StoreError::CasFailed { .. }) => {}
164 Err(error) => return Err(error.into()),
165 }
166 }
167}
168
169pub async fn renew(
176 store: &dyn RootStore,
177 db: &str,
178 held: &Lease,
179 ttl_ms: i64,
180 now_unix_ms: i64,
181) -> Result<Lease, LeaseError> {
182 let name = db_root_name(db);
183 loop {
184 let current_bytes = store.get_root(&name).await?;
185 let Some(root) = current_bytes.as_deref().and_then(DbRoot::decode) else {
186 return Err(LeaseError::Lost);
187 };
188 if root.owner != held.owner || root.lease_version != held.version {
189 return Err(LeaseError::Lost);
190 }
191 let next = DbRoot {
192 lease_expires_unix_ms: now_unix_ms + ttl_ms,
193 owner_endpoint: held.endpoint.clone(),
194 ..root
195 };
196 match store
197 .cas_root(&name, current_bytes.as_deref(), &next.encode())
198 .await
199 {
200 Ok(()) => {
201 return Ok(Lease {
202 expires_unix_ms: next.lease_expires_unix_ms,
203 ..held.clone()
204 });
205 }
206 Err(StoreError::CasFailed { .. }) => {}
210 Err(error) => return Err(error.into()),
211 }
212 }
213}
214
215pub async fn verify(store: &dyn RootStore, db: &str, held: &Lease) -> Result<(), LeaseError> {
224 let root = store
225 .get_root(&db_root_name(db))
226 .await?
227 .as_deref()
228 .and_then(DbRoot::decode);
229 match root {
230 Some(root) if root.owner == held.owner && root.lease_version == held.version => Ok(()),
231 _ => Err(LeaseError::Lost),
232 }
233}
234
235pub async fn release(store: &dyn RootStore, db: &str, held: &Lease) -> Result<(), LeaseError> {
241 let name = db_root_name(db);
242 loop {
243 let current_bytes = store.get_root(&name).await?;
244 let Some(root) = current_bytes.as_deref().and_then(DbRoot::decode) else {
245 return Err(LeaseError::Lost);
246 };
247 if root.owner != held.owner || root.lease_version != held.version {
248 return Err(LeaseError::Lost);
249 }
250 let next = DbRoot {
251 lease_expires_unix_ms: 0,
252 ..root
253 };
254 match store
255 .cas_root(&name, current_bytes.as_deref(), &next.encode())
256 .await
257 {
258 Ok(()) => return Ok(()),
259 Err(StoreError::CasFailed { .. }) => {}
260 Err(error) => return Err(error.into()),
261 }
262 }
263}