1#![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#[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 {
37 retainer: Uuid,
38 expiry: Timestamp,
39 },
40 Deferred {
42 reclaimable_at: Timestamp,
43 },
44 Settled,
45}
46
47struct Record {
48 pact: Pact,
49 state: State,
50}
51
52pub struct MemoryRegistry {
54 records: Mutex<Vec<Record>>,
55 lease_millis: u64,
56}
57
58impl MemoryRegistry {
59 #[must_use]
61 pub fn new(lease_millis: u64) -> Self {
62 Self::seeded(Vec::new(), lease_millis)
63 }
64
65 #[must_use]
68 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
69 Self {
70 records: Mutex::new(
71 pacts
72 .into_iter()
73 .map(|pact| Record {
74 pact,
75 state: State::Available,
76 })
77 .collect(),
78 ),
79 lease_millis,
80 }
81 }
82
83 fn find_holder(records: &mut [Record], retainer: &Retainer) -> Option<usize> {
84 records.iter().position(|record| {
85 matches!(record.state, State::Held { retainer: held, .. } if held == retainer.id())
86 })
87 }
88}
89
90impl Registry for MemoryRegistry {
91 type Error = NotHeld;
92
93 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
94 let mut records = self
95 .records
96 .lock()
97 .expect("registry mutex should not be poisoned");
98 let claimable = records.iter().position(|record| {
99 if !dockets.contains(&record.pact.docket.as_str()) {
100 return false;
101 }
102 match record.state {
103 State::Available => true,
104 State::Held { expiry, .. } => expiry < now,
106 State::Deferred { reclaimable_at } => reclaimable_at <= now,
108 State::Settled => false,
109 }
110 });
111
112 let Some(index) = claimable else {
114 return Ok(None);
115 };
116 let retainer = Retainer::new(Uuid::new_v4());
117 let expiry = now.plus_millis(self.lease_millis);
118 records[index].state = State::Held {
119 retainer: retainer.id(),
120 expiry,
121 };
122 Ok(Some(Claim::new(
123 records[index].pact.clone(),
124 retainer,
125 expiry,
126 )))
127 }
128
129 fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error> {
130 let mut records = self
131 .records
132 .lock()
133 .expect("registry mutex should not be poisoned");
134 let index = Self::find_holder(&mut records, retainer).ok_or(NotHeld)?;
135 let State::Held { expiry, .. } = records[index].state else {
136 return Err(NotHeld);
137 };
138 if expiry < now {
140 return Err(NotHeld);
141 }
142 records[index].state = State::Held {
143 retainer: retainer.id(),
144 expiry: now.plus_millis(self.lease_millis),
145 };
146 Ok(())
147 }
148
149 fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error> {
150 self.settle(retainer)
151 }
152
153 fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error> {
154 self.settle(retainer)
155 }
156
157 fn release(&self, retainer: &Retainer, reclaimable_at: Timestamp) -> Result<(), Self::Error> {
158 let mut records = self
159 .records
160 .lock()
161 .expect("registry mutex should not be poisoned");
162 let index = Self::find_holder(&mut records, retainer).ok_or(NotHeld)?;
166 records[index].state = State::Deferred { reclaimable_at };
170 Ok(())
171 }
172}
173
174impl MemoryRegistry {
175 fn settle(&self, retainer: &Retainer) -> Result<(), NotHeld> {
178 let mut records = self
179 .records
180 .lock()
181 .expect("registry mutex should not be poisoned");
182 let index = Self::find_holder(&mut records, retainer).ok_or(NotHeld)?;
183 records[index].state = State::Settled;
184 Ok(())
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn passes_registry_conformance() {
194 pacta_conformance::run(MemoryRegistry::seeded);
195 }
196
197 fn a_pact() -> Pact {
198 Pact::new(Uuid::new_v4(), "d".to_string(), "k".to_string(), Vec::new())
199 }
200
201 #[test]
202 fn release_rejects_a_non_holder() {
203 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
204 registry
205 .claim(&["d"], Timestamp::from_millis(0))
206 .expect("claim should not error")
207 .expect("a pact should be claimable");
208 let stranger = Retainer::new(Uuid::new_v4());
209 assert_eq!(
210 registry.release(&stranger, Timestamp::from_millis(0)),
211 Err(NotHeld),
212 "release by a non-holder must be rejected, like fulfill and breach"
213 );
214 }
215
216 #[test]
217 fn a_settled_pact_cannot_be_released() {
218 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
219 let claim = registry
220 .claim(&["d"], Timestamp::from_millis(0))
221 .expect("claim should not error")
222 .expect("a pact should be claimable");
223 registry
224 .fulfill(&claim.retainer)
225 .expect("fulfill should settle");
226 assert_eq!(
227 registry.release(&claim.retainer, Timestamp::from_millis(0)),
228 Err(NotHeld),
229 "a concluded obligation has no claim to relinquish"
230 );
231 }
232}