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 };
144 match store
145 .cas_root(&name, current_bytes.as_deref(), &next.encode())
146 .await
147 {
148 Ok(()) => {
149 if legacy.is_some() {
150 let _ = store.delete_root(&legacy_name).await;
152 }
153 return Ok(Lease {
154 owner: owner.to_owned(),
155 version,
156 expires_unix_ms: next.lease_expires_unix_ms,
157 endpoint: endpoint.to_owned(),
158 });
159 }
160 Err(StoreError::CasFailed { .. }) => {}
161 Err(error) => return Err(error.into()),
162 }
163 }
164}
165
166pub async fn renew(
173 store: &dyn RootStore,
174 db: &str,
175 held: &Lease,
176 ttl_ms: i64,
177 now_unix_ms: i64,
178) -> Result<Lease, LeaseError> {
179 let name = db_root_name(db);
180 loop {
181 let current_bytes = store.get_root(&name).await?;
182 let Some(root) = current_bytes.as_deref().and_then(DbRoot::decode) else {
183 return Err(LeaseError::Lost);
184 };
185 if root.owner != held.owner || root.lease_version != held.version {
186 return Err(LeaseError::Lost);
187 }
188 let next = DbRoot {
189 lease_expires_unix_ms: now_unix_ms + ttl_ms,
190 owner_endpoint: held.endpoint.clone(),
191 ..root
192 };
193 match store
194 .cas_root(&name, current_bytes.as_deref(), &next.encode())
195 .await
196 {
197 Ok(()) => {
198 return Ok(Lease {
199 expires_unix_ms: next.lease_expires_unix_ms,
200 ..held.clone()
201 });
202 }
203 Err(StoreError::CasFailed { .. }) => {}
207 Err(error) => return Err(error.into()),
208 }
209 }
210}
211
212pub async fn verify(store: &dyn RootStore, db: &str, held: &Lease) -> Result<(), LeaseError> {
221 let root = store
222 .get_root(&db_root_name(db))
223 .await?
224 .as_deref()
225 .and_then(DbRoot::decode);
226 match root {
227 Some(root) if root.owner == held.owner && root.lease_version == held.version => Ok(()),
228 _ => Err(LeaseError::Lost),
229 }
230}
231
232pub async fn release(store: &dyn RootStore, db: &str, held: &Lease) -> Result<(), LeaseError> {
238 let name = db_root_name(db);
239 loop {
240 let current_bytes = store.get_root(&name).await?;
241 let Some(root) = current_bytes.as_deref().and_then(DbRoot::decode) else {
242 return Err(LeaseError::Lost);
243 };
244 if root.owner != held.owner || root.lease_version != held.version {
245 return Err(LeaseError::Lost);
246 }
247 let next = DbRoot {
248 lease_expires_unix_ms: 0,
249 ..root
250 };
251 match store
252 .cas_root(&name, current_bytes.as_deref(), &next.encode())
253 .await
254 {
255 Ok(()) => return Ok(()),
256 Err(StoreError::CasFailed { .. }) => {}
257 Err(error) => return Err(error.into()),
258 }
259 }
260}