1#![forbid(unsafe_code)]
15#![warn(missing_docs)]
16
17use std::sync::Mutex;
18
19use pacta_contract::lifecycle::{self, State};
20use pacta_contract::{Claim, Pact, Registry, Retainer, Timestamp, Transition};
21use uuid::Uuid;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct NotHeld;
27
28impl std::fmt::Display for NotHeld {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "retainer is not the current holder of any claim")
31 }
32}
33
34impl std::error::Error for NotHeld {}
35
36impl From<lifecycle::NotCurrentHolder> for NotHeld {
37 fn from(_: lifecycle::NotCurrentHolder) -> Self {
38 NotHeld
39 }
40}
41
42struct Record {
43 pact: Pact,
44 state: State,
45}
46
47struct Store {
51 records: Mutex<Vec<Record>>,
52 lease_millis: u64,
53}
54
55impl Store {
56 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
57 Self {
58 records: Mutex::new(
59 pacts
60 .into_iter()
61 .map(|pact| Record {
62 pact,
63 state: State::Available,
64 })
65 .collect(),
66 ),
67 lease_millis,
68 }
69 }
70
71 fn lease_millis(&self) -> u64 {
72 self.lease_millis
73 }
74
75 fn claim(&self, dockets: &[&str], now: Timestamp) -> Option<Claim> {
76 let mut records = self
77 .records
78 .lock()
79 .expect("registry mutex should not be poisoned");
80 let index = records.iter().position(|record| {
83 dockets.contains(&record.pact.docket.as_str())
84 && lifecycle::is_claimable(&record.state, now)
85 })?;
86 let retainer = Retainer::new(Uuid::new_v4());
88 records[index].state = lifecycle::on_claim(&retainer, now, self.lease_millis);
89 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
90 Some(Claim::new(records[index].pact.clone(), retainer, expiry))
91 }
92
93 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
103 let mut records = self
104 .records
105 .lock()
106 .expect("registry mutex should not be poisoned");
107 let record = records
108 .iter_mut()
109 .find(|record| matches!(&record.state, State::Held { retainer: held, .. } if held == retainer))
110 .ok_or(NotHeld)?;
111 record.state = transition(&record.state)?;
112 Ok(())
113 }
114}
115
116pub struct MemoryRegistry {
118 store: Store,
119}
120
121impl MemoryRegistry {
122 #[must_use]
124 pub fn new(lease_millis: u64) -> Self {
125 Self::seeded(Vec::new(), lease_millis)
126 }
127
128 #[must_use]
131 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
132 Self {
133 store: Store::seeded(pacts, lease_millis),
134 }
135 }
136}
137
138impl Registry for MemoryRegistry {
139 type Error = NotHeld;
140
141 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
142 Ok(self.store.claim(dockets, now))
143 }
144
145 fn lease_millis(&self) -> u64 {
146 self.store.lease_millis()
147 }
148
149 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), Self::Error> {
150 self.store.apply(retainer, transition)
151 }
152}
153
154#[cfg(feature = "async")]
159pub struct MemoryRegistryAsync {
160 store: Store,
161}
162
163#[cfg(feature = "async")]
164impl MemoryRegistryAsync {
165 #[must_use]
167 pub fn new(lease_millis: u64) -> Self {
168 Self::seeded(Vec::new(), lease_millis)
169 }
170
171 #[must_use]
173 pub fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
174 Self {
175 store: Store::seeded(pacts, lease_millis),
176 }
177 }
178}
179
180#[cfg(feature = "async")]
181impl pacta_contract::AsyncRegistry for MemoryRegistryAsync {
182 type Error = NotHeld;
183
184 async fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
185 Ok(self.store.claim(dockets, now))
186 }
187
188 fn lease_millis(&self) -> u64 {
189 self.store.lease_millis()
190 }
191
192 async fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
193 self.store.apply(retainer, transition)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use std::marker::PhantomData;
204 use std::rc::Rc;
205
206 struct LocalRegistry {
207 inner: MemoryRegistry,
208 _local: Rc<()>,
209 }
210
211 impl LocalRegistry {
212 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
213 Self {
214 inner: MemoryRegistry::seeded(pacts, lease_millis),
215 _local: Rc::new(()),
216 }
217 }
218 }
219
220 impl Registry for LocalRegistry {
221 type Error = NotHeld;
222
223 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
224 self.inner.claim(dockets, now)
225 }
226
227 fn lease_millis(&self) -> u64 {
228 self.inner.lease_millis()
229 }
230
231 fn apply(
232 &self,
233 retainer: &Retainer,
234 transition: &Transition<'_>,
235 ) -> Result<(), Self::Error> {
236 self.inner.apply(retainer, transition)
237 }
238 }
239
240 #[cfg(feature = "async")]
241 struct LocalRegistryAsync {
242 inner: MemoryRegistryAsync,
243 _local: Rc<()>,
244 }
245
246 #[cfg(feature = "async")]
247 impl LocalRegistryAsync {
248 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
249 Self {
250 inner: MemoryRegistryAsync::seeded(pacts, lease_millis),
251 _local: Rc::new(()),
252 }
253 }
254 }
255
256 #[cfg(feature = "async")]
257 impl pacta_contract::AsyncRegistry for LocalRegistryAsync {
258 type Error = NotHeld;
259
260 async fn claim(
261 &self,
262 dockets: &[&str],
263 now: Timestamp,
264 ) -> Result<Option<Claim>, Self::Error> {
265 pacta_contract::AsyncRegistry::claim(&self.inner, dockets, now).await
266 }
267
268 fn lease_millis(&self) -> u64 {
269 pacta_contract::AsyncRegistry::lease_millis(&self.inner)
270 }
271
272 async fn apply(
273 &self,
274 retainer: &Retainer,
275 transition: &Transition<'_>,
276 ) -> Result<(), Self::Error> {
277 pacta_contract::AsyncRegistry::apply(&self.inner, retainer, transition).await
278 }
279 }
280
281 #[cfg(feature = "async")]
282 #[derive(Clone, Copy)]
283 struct LocalDriver(PhantomData<Rc<()>>);
284
285 #[cfg(feature = "async")]
286 impl pacta_conformance::BlockingDriver for LocalDriver {
287 fn drive<F: core::future::Future>(&self, future: F) -> F::Output {
288 pacta_conformance::BlockingDriver::drive(&pacta_conformance::SelfProgress, future)
289 }
290 }
291
292 #[test]
293 fn passes_registry_conformance() {
294 pacta_conformance::run(MemoryRegistry::seeded);
295 }
296
297 #[test]
298 fn local_sync_backend_passes_sequential_conformance() {
299 pacta_conformance::run(LocalRegistry::seeded);
300 }
301
302 #[test]
305 fn passes_sync_contention() {
306 pacta_conformance::run_contention(MemoryRegistry::seeded);
307 }
308
309 fn a_pact() -> Pact {
310 Pact::new(Uuid::new_v4(), "d".to_string(), "k".to_string(), Vec::new())
311 }
312
313 #[test]
314 fn release_rejects_a_non_holder() {
315 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
316 registry
317 .claim(&["d"], Timestamp::from_millis(0))
318 .expect("claim should not error")
319 .expect("a pact should be claimable");
320 let stranger = Retainer::new(Uuid::new_v4());
321 assert_eq!(
322 registry.release(&stranger, Timestamp::from_millis(0)),
323 Err(NotHeld),
324 "release by a non-holder must be rejected, like fulfill and breach"
325 );
326 }
327
328 #[test]
329 fn a_settled_pact_cannot_be_released() {
330 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
331 let claim = registry
332 .claim(&["d"], Timestamp::from_millis(0))
333 .expect("claim should not error")
334 .expect("a pact should be claimable");
335 registry
336 .fulfill(&claim.retainer)
337 .expect("fulfill should settle");
338 assert_eq!(
339 registry.release(&claim.retainer, Timestamp::from_millis(0)),
340 Err(NotHeld),
341 "a concluded obligation has no claim to relinquish"
342 );
343 }
344
345 #[test]
350 fn apply_rejects_a_stranger_even_with_an_any_state_transition() {
351 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
352 let claim = registry
353 .claim(&["d"], Timestamp::from_millis(0))
354 .expect("claim should not error")
355 .expect("a pact should be claimable");
356 let stranger = Retainer::new(Uuid::new_v4());
357 let accept_any = |_state: &State| Ok::<State, lifecycle::NotCurrentHolder>(State::Settled);
360 assert_eq!(
361 registry.apply(&stranger, &accept_any),
362 Err(NotHeld),
363 "a retainer that holds no record cannot apply, even an any-state transition"
364 );
365 registry
367 .fulfill(&claim.retainer)
368 .expect("the held state was untouched, so the holder still settles");
369 }
370
371 #[test]
374 fn apply_admits_the_true_holder() {
375 let registry = MemoryRegistry::seeded(vec![a_pact()], 1000);
376 let claim = registry
377 .claim(&["d"], Timestamp::from_millis(0))
378 .expect("claim should not error")
379 .expect("a pact should be claimable");
380 registry
381 .heartbeat(&claim.retainer, Timestamp::from_millis(500))
382 .expect("the holder's heartbeat extends the lease");
383 registry
384 .release(&claim.retainer, Timestamp::from_millis(0))
385 .expect("the holder releases");
386 assert_eq!(
387 registry.fulfill(&claim.retainer),
388 Err(NotHeld),
389 "release rotated authority, so the prior retainer no longer holds a record"
390 );
391 }
392
393 #[cfg(feature = "async")]
396 #[test]
397 fn passes_async_conformance() {
398 pacta_conformance::run_async(MemoryRegistryAsync::seeded);
399 }
400
401 #[cfg(feature = "async")]
402 #[test]
403 fn local_async_backend_passes_ready_future_conformance() {
404 pacta_conformance::run_async(LocalRegistryAsync::seeded);
405 }
406
407 #[cfg(feature = "async")]
408 #[test]
409 fn local_async_backend_and_driver_pass_runtime_compatible_conformance() {
410 pacta_conformance::run_async_with(LocalRegistryAsync::seeded, LocalDriver(PhantomData));
411 }
412
413 #[cfg(feature = "async")]
416 #[tokio::test]
417 async fn async_apply_rejects_a_stranger_even_with_an_any_state_transition() {
418 use pacta_contract::AsyncRegistry;
419
420 let registry = MemoryRegistryAsync::seeded(vec![a_pact()], 1000);
421 let claim = registry
422 .claim(&["d"], Timestamp::from_millis(0))
423 .await
424 .expect("claim should not error")
425 .expect("a pact should be claimable");
426 let stranger = Retainer::new(Uuid::new_v4());
427 let accept_any = |_state: &State| Ok::<State, lifecycle::NotCurrentHolder>(State::Settled);
428 assert_eq!(
429 registry.apply(&stranger, &accept_any).await,
430 Err(NotHeld),
431 "the async binding also locates by retainer"
432 );
433 registry
434 .fulfill(&claim.retainer)
435 .await
436 .expect("the held state was untouched, so the holder still settles");
437 }
438
439 #[cfg(feature = "async")]
444 #[test]
445 fn passes_async_contention() {
446 pacta_conformance::run_async_contention(MemoryRegistryAsync::seeded);
447 }
448}