Skip to main content

tsoracle_core/
lease.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//  https://www.tsoracle.rs
8//
9//  Copyright (c) 2026 Prisma Risk
10//
11//  Licensed under the Apache License, Version 2.0 (the "License");
12//  you may not use this file except in compliance with the License.
13//  You may obtain a copy of the License at
14//
15//      https://www.apache.org/licenses/LICENSE-2.0
16//
17//  Unless required by applicable law or agreed to in writing, software
18//  distributed under the License is distributed on an "AS IS" BASIS,
19//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20//  See the License for the specific language governing permissions and
21//  limitations under the License.
22//
23
24//! Lease records and the pure lease-table state machine (sync, no I/O).
25//!
26//! A lease grants an opaque `holder` the right to stamp timestamps with
27//! `physical_ms <= ts_upper_bound` until `expires_at_ms`. The table decides
28//! acquire/renew/release/supersede outcomes; durability and clocks live in
29//! the server + driver layers (prepare/commit split, like the allocator).
30//!
31//! Holder is opaque bytes; the table only compares holders for byte equality
32//! (the holder group key). `holder_epoch` is the supersede ordering within a
33//! holder group. Callers choose how to encode a holder group and epoch into
34//! those two fields; the table treats them as opaque ordering inputs.
35//!
36//! The record carries `holder_epoch` (required to evaluate supersede) and
37//! `ttl_ms` (required because `RenewLease { lease_id }` carries no TTL:
38//! renewal re-arms the acquire-time TTL). Expiry is `now_ms >= expires_at_ms`.
39//! Superseded-but-unexpired leases still count toward the frontier; timestamps
40//! are never reissued, so there is no reclamation, only frontier accounting.
41
42use std::collections::BTreeMap;
43
44/// Maximum accepted length of the opaque holder key, in bytes.
45pub const MAX_LEASE_HOLDER_LEN: usize = 128;
46/// Default server-enforced lower bound on requested lease TTLs (5 s).
47pub const DEFAULT_LEASE_TTL_FLOOR_MS: u64 = 5_000;
48/// Default server-enforced upper bound on requested lease TTLs (300 s).
49pub const DEFAULT_LEASE_TTL_CEILING_MS: u64 = 300_000;
50
51/// Durable lease state.
52///
53/// `lease_id` is the acquire-time `ts_upper_bound` (a committed-high-water
54/// value); strict monotonicity of the durable high-water makes it unique across
55/// grants, epochs, and failovers.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct LeaseRecord {
58    pub lease_id: u64,
59    /// Opaque holder-group key (1..=MAX_LEASE_HOLDER_LEN bytes); compared only
60    /// for byte equality.
61    pub holder: Vec<u8>,
62    /// Supersede ordering within a holder group.
63    pub holder_epoch: u64,
64    /// Acquire-time TTL, re-armed verbatim by every renewal.
65    pub ttl_ms: u64,
66    /// Highest physical_ms the holder may stamp. Always a value the durable
67    /// high-water has reached.
68    pub ts_upper_bound: u64,
69    /// Server-clock expiry; the lease stops counting toward the safe frontier
70    /// at `now_ms >= expires_at_ms`.
71    pub expires_at_ms: u64,
72    /// True once a higher-epoch acquire for the same holder group landed.
73    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
102/// Validate an AcquireLease request against server TTL policy. Floor and
103/// ceiling are server configuration.
104pub 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/// Outcome of `prepare_acquire`.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub enum AcquireDecision {
126    /// Same holder + same epoch with a live active lease: no side effects,
127    /// return the live lease as-is.
128    Idempotent(LeaseRecord),
129    /// Grant a fresh lease; `supersedes` names the active lease being
130    /// atomically superseded by a strictly-higher holder epoch, if any.
131    Grant { supersedes: Option<u64> },
132}
133
134/// Pure lease table keyed by lease_id.
135///
136/// Prepare methods are read-only decisions; commit methods apply state the
137/// caller has already persisted.
138#[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    /// Fence-time seeding from the durable record set; entries already expired
149    /// at `now_ms` are dropped because they can never count again.
150    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    /// Live record to surrender, or `None` when the release is a no-op.
229    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    /// Frontier lease term: min over unexpired leases, active or superseded.
239    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    /// The unexpired record set, for durable persistence as absolute state.
248    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    /// The live set as it will be after applying an upsert, supersede, or
257    /// removal. The server persists this projection, then commits the same
258    /// mutation.
259    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}