1use std::collections::BTreeMap;
43
44pub const MAX_LEASE_HOLDER_LEN: usize = 128;
46pub const DEFAULT_LEASE_TTL_FLOOR_MS: u64 = 5_000;
48pub const DEFAULT_LEASE_TTL_CEILING_MS: u64 = 300_000;
50
51#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct LeaseRecord {
58 pub lease_id: u64,
59 pub holder: Vec<u8>,
62 pub holder_epoch: u64,
64 pub ttl_ms: u64,
66 pub ts_upper_bound: u64,
69 pub expires_at_ms: u64,
72 pub superseded: bool,
74}
75
76impl LeaseRecord {
77 pub fn is_expired(&self, now_ms: u64) -> bool {
78 now_ms >= self.expires_at_ms
79 }
80}
81
82#[derive(Debug, thiserror::Error, PartialEq, Eq)]
83pub enum LeaseError {
84 #[error("lease holder must be 1..={MAX_LEASE_HOLDER_LEN} bytes, got {0}")]
85 HolderLenOutOfRange(usize),
86 #[error("ttl_ms {ttl_ms} outside server bounds [{floor_ms}, {ceiling_ms}]")]
87 TtlOutOfRange {
88 ttl_ms: u64,
89 floor_ms: u64,
90 ceiling_ms: u64,
91 },
92 #[error("holder epoch {requested} is not above the active lease's epoch {held}")]
93 HolderEpochStale { requested: u64, held: u64 },
94 #[error("unknown lease id {0}")]
95 UnknownLease(u64),
96 #[error("lease {lease_id} expired at {expired_at_ms} ms; acquire a new lease")]
97 LeaseExpired { lease_id: u64, expired_at_ms: u64 },
98 #[error("lease {lease_id} was superseded by holder epoch {by_epoch}")]
99 LeaseSuperseded { lease_id: u64, by_epoch: u64 },
100}
101
102pub fn validate_lease_request(
105 holder: &[u8],
106 ttl_ms: u64,
107 floor_ms: u64,
108 ceiling_ms: u64,
109) -> Result<(), LeaseError> {
110 if holder.is_empty() || holder.len() > MAX_LEASE_HOLDER_LEN {
111 return Err(LeaseError::HolderLenOutOfRange(holder.len()));
112 }
113 if ttl_ms < floor_ms || ttl_ms > ceiling_ms {
114 return Err(LeaseError::TtlOutOfRange {
115 ttl_ms,
116 floor_ms,
117 ceiling_ms,
118 });
119 }
120 Ok(())
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
125pub enum AcquireDecision {
126 Idempotent(LeaseRecord),
129 Grant { supersedes: Option<u64> },
132}
133
134#[derive(Debug, Default)]
139pub struct LeaseTable {
140 records: BTreeMap<u64, LeaseRecord>,
141}
142
143impl LeaseTable {
144 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub fn seed(&mut self, records: Vec<LeaseRecord>, now_ms: u64) {
151 self.records = records
152 .into_iter()
153 .filter(|r| !r.is_expired(now_ms))
154 .map(|r| (r.lease_id, r))
155 .collect();
156 }
157
158 pub fn clear(&mut self) {
159 self.records.clear();
160 }
161
162 fn active_for(&self, holder: &[u8], now_ms: u64) -> Option<&LeaseRecord> {
163 self.records
164 .values()
165 .find(|r| !r.superseded && !r.is_expired(now_ms) && r.holder == holder)
166 }
167
168 pub fn prepare_acquire(
169 &self,
170 holder: &[u8],
171 holder_epoch: u64,
172 now_ms: u64,
173 ) -> Result<AcquireDecision, LeaseError> {
174 match self.active_for(holder, now_ms) {
175 None => Ok(AcquireDecision::Grant { supersedes: None }),
176 Some(active) if active.holder_epoch == holder_epoch => {
177 Ok(AcquireDecision::Idempotent(active.clone()))
178 }
179 Some(active) if active.holder_epoch < holder_epoch => Ok(AcquireDecision::Grant {
180 supersedes: Some(active.lease_id),
181 }),
182 Some(active) => Err(LeaseError::HolderEpochStale {
183 requested: holder_epoch,
184 held: active.holder_epoch,
185 }),
186 }
187 }
188
189 pub fn commit_acquire(&mut self, record: LeaseRecord, supersedes: Option<u64>, now_ms: u64) {
190 if let Some(id) = supersedes
191 && let Some(old) = self.records.get_mut(&id)
192 {
193 old.superseded = true;
194 }
195 self.records.insert(record.lease_id, record);
196 self.prune_expired(now_ms);
197 }
198
199 pub fn prepare_renew(&self, lease_id: u64, now_ms: u64) -> Result<LeaseRecord, LeaseError> {
200 let record = self
201 .records
202 .get(&lease_id)
203 .ok_or(LeaseError::UnknownLease(lease_id))?;
204 if record.is_expired(now_ms) {
205 return Err(LeaseError::LeaseExpired {
206 lease_id,
207 expired_at_ms: record.expires_at_ms,
208 });
209 }
210 if record.superseded {
211 let by_epoch = self
212 .records
213 .values()
214 .filter(|r| r.holder == record.holder && r.holder_epoch > record.holder_epoch)
215 .map(|r| r.holder_epoch)
216 .max()
217 .unwrap_or(record.holder_epoch);
218 return Err(LeaseError::LeaseSuperseded { lease_id, by_epoch });
219 }
220 Ok(record.clone())
221 }
222
223 pub fn commit_renew(&mut self, record: LeaseRecord, now_ms: u64) {
224 self.records.insert(record.lease_id, record);
225 self.prune_expired(now_ms);
226 }
227
228 pub fn prepare_release(&self, lease_id: u64) -> Option<LeaseRecord> {
230 self.records.get(&lease_id).cloned()
231 }
232
233 pub fn commit_release(&mut self, lease_id: u64, now_ms: u64) {
234 self.records.remove(&lease_id);
235 self.prune_expired(now_ms);
236 }
237
238 pub fn min_unexpired_bound(&self, now_ms: u64) -> Option<u64> {
240 self.records
241 .values()
242 .filter(|r| !r.is_expired(now_ms))
243 .map(|r| r.ts_upper_bound)
244 .min()
245 }
246
247 pub fn live_set(&self, now_ms: u64) -> Vec<LeaseRecord> {
249 self.records
250 .values()
251 .filter(|r| !r.is_expired(now_ms))
252 .cloned()
253 .collect()
254 }
255
256 pub fn projected_live_set(
260 &self,
261 upsert: Option<&LeaseRecord>,
262 supersede: Option<u64>,
263 remove: Option<u64>,
264 now_ms: u64,
265 ) -> Vec<LeaseRecord> {
266 let mut set: BTreeMap<u64, LeaseRecord> = self
267 .records
268 .iter()
269 .filter(|(_, r)| !r.is_expired(now_ms))
270 .map(|(id, r)| (*id, r.clone()))
271 .collect();
272 if let Some(id) = supersede
273 && let Some(old) = set.get_mut(&id)
274 {
275 old.superseded = true;
276 }
277 if let Some(id) = remove {
278 set.remove(&id);
279 }
280 if let Some(record) = upsert {
281 set.insert(record.lease_id, record.clone());
282 }
283 set.into_values().collect()
284 }
285
286 fn prune_expired(&mut self, now_ms: u64) {
287 self.records.retain(|_, r| !r.is_expired(now_ms));
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 fn rec(id: u64, holder: &[u8], epoch: u64, bound: u64, expires: u64) -> LeaseRecord {
296 LeaseRecord {
297 lease_id: id,
298 holder: holder.to_vec(),
299 holder_epoch: epoch,
300 ttl_ms: 10_000,
301 ts_upper_bound: bound,
302 expires_at_ms: expires,
303 superseded: false,
304 }
305 }
306
307 #[test]
308 fn validate_rejects_bad_holder_and_ttl() {
309 assert!(matches!(
310 validate_lease_request(&[], 10_000, 5_000, 300_000),
311 Err(LeaseError::HolderLenOutOfRange(0))
312 ));
313 assert!(matches!(
314 validate_lease_request(&[0u8; MAX_LEASE_HOLDER_LEN + 1], 10_000, 5_000, 300_000),
315 Err(LeaseError::HolderLenOutOfRange(_))
316 ));
317 assert!(matches!(
318 validate_lease_request(b"g1", 4_999, 5_000, 300_000),
319 Err(LeaseError::TtlOutOfRange { .. })
320 ));
321 assert!(matches!(
322 validate_lease_request(b"g1", 300_001, 5_000, 300_000),
323 Err(LeaseError::TtlOutOfRange { .. })
324 ));
325 assert!(validate_lease_request(b"g1", 5_000, 5_000, 300_000).is_ok());
326 assert!(validate_lease_request(b"g1", 300_000, 5_000, 300_000).is_ok());
327 }
328
329 #[test]
330 fn fresh_holder_gets_grant_without_supersede() {
331 let table = LeaseTable::new();
332 assert_eq!(
333 table.prepare_acquire(b"g1", 1, 1_000).unwrap(),
334 AcquireDecision::Grant { supersedes: None }
335 );
336 }
337
338 #[test]
339 fn same_epoch_reacquire_is_idempotent() {
340 let mut table = LeaseTable::new();
341 let live = rec(100, b"g1", 1, 100, 11_000);
342 table.commit_acquire(live.clone(), None, 1_000);
343 assert_eq!(
344 table.prepare_acquire(b"g1", 1, 2_000).unwrap(),
345 AcquireDecision::Idempotent(live)
346 );
347 }
348
349 #[test]
350 fn higher_epoch_supersedes_and_lower_epoch_is_rejected() {
351 let mut table = LeaseTable::new();
352 table.commit_acquire(rec(100, b"g1", 5, 100, 11_000), None, 1_000);
353 assert_eq!(
354 table.prepare_acquire(b"g1", 6, 2_000).unwrap(),
355 AcquireDecision::Grant {
356 supersedes: Some(100)
357 }
358 );
359 assert!(matches!(
360 table.prepare_acquire(b"g1", 4, 2_000),
361 Err(LeaseError::HolderEpochStale {
362 requested: 4,
363 held: 5
364 })
365 ));
366 }
367
368 #[test]
369 fn superseded_lease_counts_toward_frontier_until_expiry() {
370 let mut table = LeaseTable::new();
371 table.commit_acquire(rec(100, b"g1", 1, 100, 11_000), None, 1_000);
372 table.commit_acquire(rec(200, b"g1", 2, 200, 12_000), Some(100), 2_000);
373 assert_eq!(table.min_unexpired_bound(3_000), Some(100));
374 assert_eq!(table.min_unexpired_bound(11_000), Some(200));
375 assert!(matches!(
376 table.prepare_renew(100, 3_000),
377 Err(LeaseError::LeaseSuperseded {
378 lease_id: 100,
379 by_epoch: 2
380 })
381 ));
382 }
383
384 #[test]
385 fn renew_unknown_and_expired_are_distinct_errors() {
386 let mut table = LeaseTable::new();
387 table.commit_acquire(rec(100, b"g1", 1, 100, 11_000), None, 1_000);
388 assert!(matches!(
389 table.prepare_renew(999, 2_000),
390 Err(LeaseError::UnknownLease(999))
391 ));
392 assert!(matches!(
393 table.prepare_renew(100, 11_000),
394 Err(LeaseError::LeaseExpired {
395 lease_id: 100,
396 expired_at_ms: 11_000
397 })
398 ));
399 assert_eq!(table.prepare_renew(100, 10_999).unwrap().lease_id, 100);
400 }
401
402 #[test]
403 fn release_is_idempotent_and_removes_from_frontier() {
404 let mut table = LeaseTable::new();
405 table.commit_acquire(rec(100, b"g1", 1, 100, 11_000), None, 1_000);
406 assert!(table.prepare_release(100).is_some());
407 table.commit_release(100, 2_000);
408 assert_eq!(table.min_unexpired_bound(2_000), None);
409 assert!(
410 table.prepare_release(100).is_none(),
411 "second release is a no-op"
412 );
413 assert!(
414 table.prepare_release(42).is_none(),
415 "unknown release is a no-op"
416 );
417 }
418
419 #[test]
420 fn seed_drops_expired_records() {
421 let mut table = LeaseTable::new();
422 table.seed(
423 vec![rec(1, b"g1", 1, 10, 5_000), rec(2, b"g2", 1, 20, 50_000)],
424 6_000,
425 );
426 assert_eq!(table.min_unexpired_bound(6_000), Some(20));
427 assert!(matches!(
428 table.prepare_renew(1, 6_000),
429 Err(LeaseError::UnknownLease(1))
430 ));
431 }
432
433 #[test]
434 fn projected_live_set_matches_committed_state() {
435 let mut table = LeaseTable::new();
436 table.commit_acquire(rec(100, b"g1", 1, 100, 11_000), None, 1_000);
437 let incoming = rec(200, b"g1", 2, 200, 12_000);
438 let projected = table.projected_live_set(Some(&incoming), Some(100), None, 2_000);
439 table.commit_acquire(incoming, Some(100), 2_000);
440 let mut committed = table.live_set(2_000);
441 let mut projected = projected;
442 committed.sort_by_key(|r| r.lease_id);
443 projected.sort_by_key(|r| r.lease_id);
444 assert_eq!(projected, committed);
445 assert!(committed.iter().any(|r| r.lease_id == 100 && r.superseded));
446 }
447
448 #[test]
449 fn expired_active_holder_gets_fresh_grant() {
450 let mut table = LeaseTable::new();
451 table.commit_acquire(rec(100, b"g1", 3, 100, 11_000), None, 1_000);
452 assert_eq!(
453 table.prepare_acquire(b"g1", 3, 11_000).unwrap(),
454 AcquireDecision::Grant { supersedes: None }
455 );
456 }
457}