Skip to main content

pacta_memory/
lib.rs

1//! An in-memory [`Registry`] backend with real lease and lapse semantics.
2//!
3//! This is a **reference** backend, not a durable or production one: it holds pacts
4//! in memory, so nothing survives the process. It exists to demonstrate correct
5//! lifecycle semantics and to calibrate against — durable backends live outside this
6//! workspace and prove themselves against `pacta-conformance` just as this one does.
7//!
8//! It is a pure lifecycle state machine that holds
9//! pacts, leases claims for a user-supplied duration, reclaims lapsed pacts through
10//! the normal claim path, and rotates the retainer on every claim so a stale holder
11//! cannot settle. It reads no clock — time is injected into `claim` and `heartbeat`.
12
13#![forbid(unsafe_code)]
14#![warn(missing_docs)]
15
16use std::sync::Mutex;
17
18use pacta_contract::{Claim, Pact, Registry, Retainer, Timestamp};
19use uuid::Uuid;
20
21/// The error a memory registry returns when a retainer is not the current holder,
22/// or when a heartbeat arrives after its lease has already lapsed.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct NotHeld;
25
26impl std::fmt::Display for NotHeld {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "retainer is not the current holder of any claim")
29    }
30}
31
32impl std::error::Error for NotHeld {}
33
34enum State {
35    Available,
36    Held { retainer: Uuid, expiry: Timestamp },
37    Settled,
38}
39
40struct Record {
41    pact: Pact,
42    state: State,
43}
44
45/// An in-memory registry seeded with a fixed set of pacts.
46pub struct MemoryRegistry {
47    records: Mutex<Vec<Record>>,
48    lease_millis: u64,
49}
50
51impl MemoryRegistry {
52    /// Create an empty registry that leases claims for `lease_millis`.
53    #[must_use]
54    pub fn new(lease_millis: u64) -> Self {
55        Self::seeded(Vec::new(), lease_millis)
56    }
57
58    /// Create a registry holding `pacts`, each available to claim, leasing claims
59    /// for `lease_millis`.
60    #[must_use]
61    pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
62        Self {
63            records: Mutex::new(
64                pacts
65                    .into_iter()
66                    .map(|pact| Record {
67                        pact,
68                        state: State::Available,
69                    })
70                    .collect(),
71            ),
72            lease_millis,
73        }
74    }
75
76    fn find_holder(records: &mut [Record], retainer: &Retainer) -> Option<usize> {
77        records.iter().position(|record| {
78            matches!(record.state, State::Held { retainer: held, .. } if held == retainer.id())
79        })
80    }
81}
82
83impl Registry for MemoryRegistry {
84    type Error = NotHeld;
85
86    fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
87        let mut records = self
88            .records
89            .lock()
90            .expect("registry mutex should not be poisoned");
91        let claimable = records.iter().position(|record| {
92            if !dockets.contains(&record.pact.docket.as_str()) {
93                return false;
94            }
95            match record.state {
96                State::Available => true,
97                // A lapse: an expired hold is reclaimable through this claim path.
98                State::Held { expiry, .. } => expiry < now,
99                State::Settled => false,
100            }
101        });
102
103        // Mint a retainer only on a successful claim.
104        let Some(index) = claimable else {
105            return Ok(None);
106        };
107        let retainer = Retainer::new(Uuid::new_v4());
108        let expiry = now.plus_millis(self.lease_millis);
109        records[index].state = State::Held {
110            retainer: retainer.id(),
111            expiry,
112        };
113        Ok(Some(Claim::new(
114            records[index].pact.clone(),
115            retainer,
116            expiry,
117        )))
118    }
119
120    fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error> {
121        let mut records = self
122            .records
123            .lock()
124            .expect("registry mutex should not be poisoned");
125        let index = Self::find_holder(&mut records, retainer).ok_or(NotHeld)?;
126        let State::Held { expiry, .. } = records[index].state else {
127            return Err(NotHeld);
128        };
129        // Refuse to revive a lapsed lease: the holder must re-claim.
130        if expiry < now {
131            return Err(NotHeld);
132        }
133        records[index].state = State::Held {
134            retainer: retainer.id(),
135            expiry: now.plus_millis(self.lease_millis),
136        };
137        Ok(())
138    }
139
140    fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error> {
141        self.settle(retainer)
142    }
143
144    fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error> {
145        self.settle(retainer)
146    }
147}
148
149impl MemoryRegistry {
150    // fulfill and breach share the same authority check: a stale retainer no longer
151    // matches the rotated current holder, so no time is needed to reject it.
152    fn settle(&self, retainer: &Retainer) -> Result<(), NotHeld> {
153        let mut records = self
154            .records
155            .lock()
156            .expect("registry mutex should not be poisoned");
157        let index = Self::find_holder(&mut records, retainer).ok_or(NotHeld)?;
158        records[index].state = State::Settled;
159        Ok(())
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn passes_registry_conformance() {
169        pacta_conformance::run(MemoryRegistry::seeded);
170    }
171}