pacta_contract/lib.rs
1//! The isolated core contract for Pacta.
2//!
3//! Pacta operates on three axioms:
4//! 1. Registry is Lifecycle (no business logic, no retry/delay logic).
5//! 2. Execution is Middleware.
6//! 3. This contract has no dependency on other workspace crates.
7
8#![forbid(unsafe_code)]
9#![warn(missing_docs)]
10
11use serde::{Deserialize, Serialize};
12
13/// The identifier type the durable records and the fencing token are built on, re-exported from the
14/// [`uuid`] crate. A backend needs it to build a [`Pact`] (`Pact::new(id, …)`) and to mint a fresh
15/// [`Retainer`] (`Retainer::new(Uuid…)`), so it is part of the backend-author surface.
16pub use uuid::Uuid;
17
18/// A durable obligation, generated from a Signal, ready to be executed.
19/// Note the deliberate absence of `attempts`, `delay`, and `priority`.
20///
21/// Construct through [`Pact::new`]; the fields stay public for reading. The type is
22/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
23/// breaking change.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct Pact {
27 /// Stable identifier for this pact.
28 pub id: Uuid,
29 /// Logical docket from which the pact can be claimed.
30 pub docket: String,
31 /// Application-defined pact kind.
32 pub kind: String,
33 /// Business data required to fulfill the pact.
34 pub clause: Vec<u8>,
35}
36
37impl Pact {
38 /// Build a pact from its identifier, docket, kind, and clause.
39 #[must_use]
40 pub fn new(id: Uuid, docket: String, kind: String, clause: Vec<u8>) -> Self {
41 Self {
42 id,
43 docket,
44 kind,
45 clause,
46 }
47 }
48}
49
50/// A retainer: the authority token a registry issues with a claim and validates
51/// when settling it. Authority is registry-validated — a forged identifier does not
52/// match an issued claim — not proven by the type system. Construct via
53/// [`Retainer::new`] and read the identifier via [`Retainer::id`]. Derives
54/// `PartialEq`/`Eq`/`Hash` so a durable backend can index lease state by holder
55/// identity — the orphan rule makes providing these the contract's responsibility.
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub struct Retainer(Uuid);
58
59impl Retainer {
60 /// Mint a retainer from an identifier. A registry issues tokens through this.
61 #[must_use]
62 pub fn new(id: Uuid) -> Self {
63 Self(id)
64 }
65
66 /// The retainer's identifier, which a registry validates on settlement.
67 #[must_use]
68 pub fn id(&self) -> Uuid {
69 self.0
70 }
71}
72
73/// A point in time as milliseconds since an epoch the runtime chooses. This is a
74/// pure value: the core names time but never reads it. There is deliberately no
75/// `now` constructor — a runtime obtains the current time and injects it, keeping
76/// lease decisions deterministic and testable.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
78pub struct Timestamp(u64);
79
80impl Timestamp {
81 /// Build a timestamp from milliseconds since the runtime's chosen epoch.
82 #[must_use]
83 pub fn from_millis(millis: u64) -> Self {
84 Self(millis)
85 }
86
87 /// The milliseconds since the runtime's chosen epoch.
88 #[must_use]
89 pub fn as_millis(self) -> u64 {
90 self.0
91 }
92
93 /// The timestamp `millis` milliseconds after this one, saturating at the maximum.
94 #[must_use]
95 pub fn plus_millis(self, millis: u64) -> Self {
96 Self(self.0.saturating_add(millis))
97 }
98}
99
100/// A claimed pact and the retainer required to settle it.
101///
102/// Construct through [`Claim::new`]; the fields stay public for reading. The type is
103/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
104/// breaking change.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[non_exhaustive]
107pub struct Claim {
108 /// Pact claimed for execution.
109 pub pact: Pact,
110 /// Authority required to heartbeat, fulfill, or breach the claim.
111 pub retainer: Retainer,
112 /// When the claim's lease expires. After this the pact may be lapsed and
113 /// reclaimed unless the holder heartbeats first.
114 pub lease_expiry: Timestamp,
115}
116
117impl Claim {
118 /// Build a claim from a pact, the settling retainer, and the lease expiry.
119 #[must_use]
120 pub fn new(pact: Pact, retainer: Retainer, lease_expiry: Timestamp) -> Self {
121 Self {
122 pact,
123 retainer,
124 lease_expiry,
125 }
126 }
127}
128
129// The lease identity must be usable as a durable-backend key; removing the derives
130// fails this build rather than silently regressing the backend contract.
131const _: fn() = || {
132 fn assert_key<T: Eq + std::hash::Hash>() {}
133 assert_key::<Retainer>();
134};
135
136/// The lifecycle outcome an execution produces for a claimed pact.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138#[must_use]
139pub enum Outcome {
140 /// The pact was fulfilled successfully.
141 Fulfilled,
142 /// The pact could not be fulfilled and must be breached.
143 Breached,
144}
145
146/// The lifecycle conclusion applied to a claim, currently a fulfilled or breached
147/// [`Outcome`].
148pub type Settlement = Outcome;
149
150/// The pure lifecycle state machine every `Registry` backend composes over.
151///
152/// This is the single source of the pact lifecycle *semantics* — the claim-eligibility
153/// predicate, the state transitions, the current-holder authority check, and the lease
154/// arithmetic. A backend owns its own storage and mints its own retainer (a fencing
155/// value); it delegates every eligibility decision and transition here, so the semantics
156/// are defined once and cannot drift between backends (or between a synchronous and a
157/// future asynchronous binding).
158///
159/// It is colorless and sans-I/O: it reads no clock (time is an injected parameter),
160/// performs no I/O, and mints nothing non-deterministic (the retainer is supplied by the
161/// caller). Named `lifecycle` to distinguish it from the executor step-driver
162/// [`kernel`], which is a different pure machine.
163pub mod lifecycle {
164 use crate::{Retainer, Timestamp};
165
166 /// A pact's position in its claim lifecycle: the pure state a backend maps to its own storage.
167 /// The backend owns where it lives; this owns what it means.
168 ///
169 /// For the 0.2 series this is a **closed** enumeration of exactly these four variants — it is
170 /// deliberately not `#[non_exhaustive]` — so a backend author knows the complete set of states to
171 /// represent and can match it exhaustively, distinct from the growing `#[non_exhaustive]`
172 /// protocol enums (`Directive`/`Notice`/`StepResult`) elsewhere in this crate. (This is a
173 /// stability statement for 0.2.x, not a promise never to evolve the model in a later minor.)
174 #[derive(Debug, Clone, PartialEq, Eq)]
175 #[must_use]
176 pub enum State {
177 /// Never claimed, or freshly seeded: immediately claimable.
178 Available,
179 /// Held under a lease by `retainer` until `expiry`. Claimable again only once
180 /// the lease has lapsed (`expiry < now`), which rotates authority away.
181 Held {
182 /// The current holder's authority token.
183 retainer: Retainer,
184 /// When the lease expires.
185 expiry: Timestamp,
186 },
187 /// Released non-terminally: claimable again only at or after `reclaimable_at`.
188 Deferred {
189 /// The instant at or after which the pact may be reclaimed.
190 reclaimable_at: Timestamp,
191 },
192 /// Concluded (fulfilled or breached): never claimable again.
193 ///
194 /// This is the *model* (and reference-backend) representation of a concluded obligation, not
195 /// a required storage obligation. A durable backend MAY represent settled by **removing the
196 /// row** — a load of the absent row returns no state, so the pact is trivially not claimable
197 /// and the prior retainer can no longer transition it, which is the whole of what settlement
198 /// guarantees. Do not assume a settled pact persists.
199 Settled,
200 }
201
202 /// A transition was attempted by something that is not the state's current holder —
203 /// a stale retainer, or a state (available, deferred, settled) with no holder at all.
204 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
205 pub struct NotCurrentHolder;
206
207 impl std::fmt::Display for NotCurrentHolder {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 write!(f, "retainer is not the current holder of this pact")
210 }
211 }
212
213 impl std::error::Error for NotCurrentHolder {}
214
215 /// The lease expiry for a claim taken at `now` for `lease_millis` — the single
216 /// source of the lease arithmetic.
217 #[must_use]
218 pub fn lease_expiry(now: Timestamp, lease_millis: u64) -> Timestamp {
219 now.plus_millis(lease_millis)
220 }
221
222 /// Whether a pact in `state` may be claimed at `now`: the eligibility invariant.
223 /// `Available` always; a `Held` lease that has lapsed; a `Deferred` pact at or past
224 /// its instant; never a `Settled` one. Only positive, unambiguous eligibility.
225 #[must_use]
226 pub fn is_claimable(state: &State, now: Timestamp) -> bool {
227 match state {
228 State::Available => true,
229 State::Held { expiry, .. } => *expiry < now,
230 State::Deferred { reclaimable_at } => *reclaimable_at <= now,
231 State::Settled => false,
232 }
233 }
234
235 /// The state a successful claim produces: `Held` by `retainer` until the lease
236 /// expiry for `now`/`lease_millis`. The backend mints `retainer` and passes it in.
237 pub fn on_claim(retainer: &Retainer, now: Timestamp, lease_millis: u64) -> State {
238 State::Held {
239 retainer: retainer.clone(),
240 expiry: lease_expiry(now, lease_millis),
241 }
242 }
243
244 /// The state a heartbeat produces: the lease extended to the expiry for
245 /// `now`/`lease_millis`, provided `retainer` currently holds `state` and the lease
246 /// has not already lapsed. A lapsed lease is not revived — the holder must re-claim.
247 pub fn on_heartbeat(
248 state: &State,
249 retainer: &Retainer,
250 now: Timestamp,
251 lease_millis: u64,
252 ) -> Result<State, NotCurrentHolder> {
253 match state {
254 State::Held {
255 retainer: held,
256 expiry,
257 } if held == retainer && *expiry >= now => Ok(State::Held {
258 retainer: retainer.clone(),
259 expiry: lease_expiry(now, lease_millis),
260 }),
261 _ => Err(NotCurrentHolder),
262 }
263 }
264
265 /// The state a settlement produces: `Settled`, provided `retainer` currently holds
266 /// `state`. Fulfill and breach share this — the lifecycle state records that the
267 /// obligation concluded, not which outcome concluded it.
268 pub fn on_settle(state: &State, retainer: &Retainer) -> Result<State, NotCurrentHolder> {
269 if is_current_holder(state, retainer) {
270 Ok(State::Settled)
271 } else {
272 Err(NotCurrentHolder)
273 }
274 }
275
276 /// The state a release produces: `Deferred` until `reclaimable_at`, provided
277 /// `retainer` currently holds `state`. Non-terminal; rotates authority away.
278 pub fn on_release(
279 state: &State,
280 retainer: &Retainer,
281 reclaimable_at: Timestamp,
282 ) -> Result<State, NotCurrentHolder> {
283 if is_current_holder(state, retainer) {
284 Ok(State::Deferred { reclaimable_at })
285 } else {
286 Err(NotCurrentHolder)
287 }
288 }
289
290 fn is_current_holder(state: &State, retainer: &Retainer) -> bool {
291 matches!(state, State::Held { retainer: held, .. } if held == retainer)
292 }
293
294 #[cfg(test)]
295 mod tests {
296 use super::*;
297 use uuid::Uuid;
298
299 fn retainer() -> Retainer {
300 Retainer::new(Uuid::new_v4())
301 }
302
303 #[test]
304 fn eligibility_covers_each_state() {
305 let now = Timestamp::from_millis(100);
306 assert!(is_claimable(&State::Available, now));
307 // A held lease is claimable only once lapsed.
308 assert!(!is_claimable(
309 &State::Held {
310 retainer: retainer(),
311 expiry: Timestamp::from_millis(101)
312 },
313 now
314 ));
315 assert!(is_claimable(
316 &State::Held {
317 retainer: retainer(),
318 expiry: Timestamp::from_millis(99)
319 },
320 now
321 ));
322 // A deferred pact is claimable at or past its instant.
323 assert!(!is_claimable(
324 &State::Deferred {
325 reclaimable_at: Timestamp::from_millis(101)
326 },
327 now
328 ));
329 assert!(is_claimable(
330 &State::Deferred {
331 reclaimable_at: Timestamp::from_millis(100)
332 },
333 now
334 ));
335 assert!(!is_claimable(&State::Settled, now));
336 }
337
338 #[test]
339 fn transitions_require_the_current_holder() {
340 let holder = retainer();
341 let held = State::Held {
342 retainer: holder.clone(),
343 expiry: Timestamp::from_millis(200),
344 };
345 let stranger = retainer();
346
347 assert_eq!(on_settle(&held, &stranger), Err(NotCurrentHolder));
348 assert_eq!(
349 on_release(&held, &stranger, Timestamp::from_millis(0)),
350 Err(NotCurrentHolder)
351 );
352 assert_eq!(on_settle(&State::Settled, &holder), Err(NotCurrentHolder));
353
354 assert_eq!(on_settle(&held, &holder), Ok(State::Settled));
355 assert_eq!(
356 on_release(&held, &holder, Timestamp::from_millis(500)),
357 Ok(State::Deferred {
358 reclaimable_at: Timestamp::from_millis(500)
359 })
360 );
361 }
362
363 #[test]
364 fn heartbeat_refreshes_but_does_not_revive_a_lapsed_lease() {
365 let holder = retainer();
366 let held = State::Held {
367 retainer: holder.clone(),
368 expiry: Timestamp::from_millis(200),
369 };
370 // Live lease refreshes.
371 assert_eq!(
372 on_heartbeat(&held, &holder, Timestamp::from_millis(150), 100),
373 Ok(State::Held {
374 retainer: holder.clone(),
375 expiry: Timestamp::from_millis(250)
376 })
377 );
378 // Lapsed lease is not revived.
379 assert_eq!(
380 on_heartbeat(&held, &holder, Timestamp::from_millis(201), 100),
381 Err(NotCurrentHolder)
382 );
383 }
384 }
385}
386
387/// A pure kernel transition decision — a [`lifecycle`] `on_X` — passed to the transition port
388/// [`Registry::apply`] (and its async twin). The same type is used by both bindings, so the port is
389/// literally one shape.
390///
391/// The `Send + Sync` bound is on the transition **closure**: it lets a backend hold the decision
392/// across its own atomic scope or hand it to a worker thread. It does **not** make the async
393/// binding's `apply` *future* `Send` — future coloring stays the consumer's (the async binding is
394/// deliberately `Send`-agnostic at its futures). A backend that needs a `Send` `apply` future
395/// satisfies that at its own concrete call site, not from this bound.
396pub type Transition<'a> = dyn Fn(&lifecycle::State) -> Result<lifecycle::State, lifecycle::NotCurrentHolder>
397 + Send
398 + Sync
399 + 'a;
400
401/// The asynchronous binding of the [`Registry`] contract, available behind the `async` feature.
402/// [`AsyncRegistry`] is the same five-op contract over the same [`Transition`] port, made async;
403/// [`apply_via_cas`] is the optional compare-and-set helper. A consumer that does not enable `async`
404/// compiles none of it.
405#[cfg(feature = "async")]
406mod async_registry;
407#[cfg(feature = "async")]
408pub use async_registry::{AsyncRegistry, apply_via_cas};
409
410/// The Registry is the durable lifecycle-authority **port**: it preserves pacts and decides claim,
411/// lease, and settlement authority over them. It is *not itself* the pure state machine — the pure,
412/// colorless machine is [`lifecycle`], which every backend composes over; a `Registry`
413/// implementation is the I/O-owning port that persists that machine's states and enforces its
414/// authority (the async twin is [`AsyncRegistry`]).
415///
416/// A backend implements three primitives — a native [`claim`](Registry::claim) selection, a
417/// [`lease_millis`](Registry::lease_millis) accessor, and an atomic [`apply`](Registry::apply)
418/// transition port — and inherits heartbeat, fulfill, breach, and release as defaults over `apply`.
419/// The obligations mirror the async binding exactly:
420///
421/// - **`claim` selects atomically, admits only an eligible pact, and rotates the retainer.** It
422/// returns only a pact [`lifecycle::is_claimable`] would admit and mints a fresh retainer, all in
423/// one atomic step. A durable backend expresses this as a native, full-scan-free selection.
424/// - **`apply` is `load → decide → store` in one atomic scope.** It loads the state held by the
425/// retainer, computes the next state through the passed [`lifecycle`] decision, and stores it
426/// atomically; a non-atomic load-to-store window lets two workers both write and breaks
427/// exactly-once and retainer fencing.
428/// - **Reclaim — not mere expiry — rotates settlement authority.** A holder whose lease lapsed but
429/// whose pact no one reclaimed is still the current holder and can still settle; authority rotates
430/// only when the pact is actually reclaimed (or released). A transition against a pact the retainer
431/// no longer holds surfaces as a not-current-holder error through the backend's `Error`.
432///
433/// `pacta-conformance` proves the *behavioral* half of this (eligibility, transitions, lapse/reclaim
434/// rotation, and — via its contention checks — at-most-once claim and settlement). It does **not**
435/// prove the *query-shape* obligation that `claim` is full-scan-free; a sequential functional suite
436/// cannot observe query cost, so that stays a backend obligation established by review.
437///
438/// Time is injected: [`claim`](Registry::claim) and [`heartbeat`](Registry::heartbeat) take the
439/// current time as a parameter, and the registry reads no ambient clock. Settlement takes no time
440/// because a rotated retainer already tells a stale holder apart from the current one.
441///
442/// The backend type itself need not be [`Send`] or [`Sync`]. A single-threaded backend may stay
443/// local; a caller that moves or shares a registry across threads adds `Send + Sync` at that
444/// concurrency boundary. In particular, generic code that needs thread shareability must write
445/// `R: Registry + Send + Sync` rather than treating it as implied by this trait.
446pub trait Registry {
447 /// Error returned by the registry implementation.
448 type Error: std::error::Error;
449
450 /// Claim a pact for execution from one of the requested dockets, using `now`
451 /// to set the new lease and to reclaim any pact whose lease already expired
452 /// without settlement — a lapse, realized through this normal claim path.
453 ///
454 /// **Obligation:** select — atomically — only a pact [`lifecycle::is_claimable`] would admit
455 /// (available, a lapsed hold, or a deferred pact past its instant; never a settled one) and mint
456 /// a fresh retainer, so reclaiming rotates authority and the prior holder can no longer settle. A
457 /// durable backend expresses this as a native, full-scan-free selection (for example SQL
458 /// `SKIP LOCKED`), not by loading the whole docket to filter in memory.
459 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error>;
460
461 /// The backend's lease duration in milliseconds, used by [`heartbeat`](Registry::heartbeat)
462 /// to compute the extended lease. Lease sizing is the backend's; the contract supplies the
463 /// mechanism, not a constant.
464 fn lease_millis(&self) -> u64;
465
466 /// Apply a lifecycle transition to the pact held by `retainer`, within the backend's own
467 /// atomic scope. `transition` is the pure kernel decision (a [`lifecycle`] `on_X`): the
468 /// backend loads the held state, computes the next state through `transition`, and applies
469 /// it atomically — it never decides the transition itself, so the lifecycle semantics stay
470 /// single-sourced in the kernel and cannot drift. A transition applied against a pact the
471 /// retainer no longer holds resolves to a not-current-holder error (the kernel's
472 /// [`NotCurrentHolder`](lifecycle::NotCurrentHolder) surfaces through `transition`). This is
473 /// the one transition port; the four transition operations below are provided over it.
474 ///
475 /// The backend owns *how* the scope is made atomic (a lock, a transaction, a native
476 /// conditional write, or compare-and-set); the contract mandates no concurrency-control
477 /// mechanism.
478 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), Self::Error>;
479
480 /// Extend the retainer's lease using `now`. A heartbeat presented after the
481 /// lease already expired is rejected: the holder must claim again rather than
482 /// revive a lapsed lease, so two holders never both hold settlement authority.
483 fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error> {
484 let lease = self.lease_millis();
485 self.apply(retainer, &|state| {
486 lifecycle::on_heartbeat(state, retainer, now, lease)
487 })
488 }
489
490 /// Mark the pact as successfully fulfilled. Rejected when the retainer is not
491 /// the current holder.
492 fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error> {
493 self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
494 }
495
496 /// Mark the pact as breached. Rejected when the retainer is not the current
497 /// holder. Shares the settlement transition with [`fulfill`](Registry::fulfill) — the
498 /// lifecycle records that the obligation concluded, not which outcome concluded it.
499 fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error> {
500 self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
501 }
502
503 /// Release the claim without concluding the obligation, making the pact
504 /// reclaimable again only at or after `reclaimable_at`.
505 ///
506 /// This is **non-terminal**: unlike [`fulfill`](Registry::fulfill) and
507 /// [`breach`](Registry::breach), it settles nothing — the pact is left to be
508 /// attempted again. The registry computes no delay: `reclaimable_at` is a
509 /// consumer-supplied instant, honored exactly as the injected `now` is honored
510 /// (compared, never computed), so backoff policy stays with the caller and `Pact`
511 /// carries no delay. A `reclaimable_at` at or before now makes the pact immediately
512 /// claimable, as a voluntary lapse. Release rotates authority like a lapse, so the
513 /// prior retainer can no longer settle or heartbeat. Rejected when the retainer is
514 /// not the current holder.
515 fn release(&self, retainer: &Retainer, reclaimable_at: Timestamp) -> Result<(), Self::Error> {
516 self.apply(retainer, &|state| {
517 lifecycle::on_release(state, retainer, reclaimable_at)
518 })
519 }
520}
521
522/// The sans-I/O lifecycle kernel.
523///
524/// The kernel is a pure state machine: it decides the next [`Directive`](kernel::Directive)
525/// from its state and absorbs [`Notice`](kernel::Notice) reports a runtime feeds
526/// back. It performs no I/O
527/// and exposes no `async fn`, so it commits to no runtime shape. It encodes the
528/// lifecycle decision table only — it adds no orchestration behavior.
529///
530/// # Advanced surface
531///
532/// This is the **advanced** tier of Pacta's public API: lower stability intent than
533/// the recommended surface (its API may evolve as the runtime story settles), though
534/// it stays a supported, governed core surface — not unsupported or slated for
535/// removal. Most consumers should compose with the `Driver` runtime (or the `pacta`
536/// facade) and never touch the kernel. Reach for it only to build a custom runtime;
537/// it is reached through `pacta-contract` directly, never through the `pacta` facade.
538///
539/// # Driving the kernel
540///
541/// A runtime drives one lifecycle step by looping: ask [`poll`](kernel::Kernel::poll)
542/// for the next [`Directive`](kernel::Directive), perform it, report the outcome back with
543/// [`on_event`](kernel::Kernel::on_event), and repeat until [`result`](kernel::Kernel::result)
544/// yields a terminal [`StepResult`](kernel::StepResult). The kernel decides *what*;
545/// the runtime performs it and injects time — the kernel reads no clock.
546///
547/// ```
548/// use pacta_contract::kernel::{Directive, Kernel, Notice, StepResult};
549/// use pacta_contract::{Claim, Outcome, Pact, Retainer, Timestamp};
550///
551/// let mut kernel = Kernel::new();
552/// let mut available = Some(Claim::new(
553/// Pact::new(Default::default(), "demo".into(), "demo".into(), Vec::new()),
554/// Retainer::new(Default::default()),
555/// Timestamp::from_millis(0),
556/// ));
557///
558/// let result = loop {
559/// if let Some(result) = kernel.result() {
560/// break result;
561/// }
562/// match kernel.poll() {
563/// Directive::Claim => kernel.on_event(Notice::Claimed(available.take())),
564/// Directive::Execute(_pact) => kernel.on_event(Notice::Executed(Outcome::Fulfilled)),
565/// Directive::Settle(_retainer, _outcome) => kernel.on_event(Notice::Settled),
566/// Directive::Idle => break StepResult::Idle,
567/// _ => unreachable!("driver handles every current kernel directive"),
568/// }
569/// };
570///
571/// assert_eq!(result, StepResult::Settled(Outcome::Fulfilled));
572/// ```
573pub mod kernel {
574 use crate::{Claim, Outcome, Pact, Retainer};
575
576 /// An instruction the kernel issues for a runtime to perform.
577 ///
578 /// `#[non_exhaustive]`: this advanced-tier protocol may gain directives, so a
579 /// runtime's match must carry a wildcard arm.
580 #[derive(Debug, Clone)]
581 #[non_exhaustive]
582 #[must_use]
583 pub enum Directive {
584 /// Claim a pact from the runtime's configured dockets.
585 Claim,
586 /// Execute the given claimed pact.
587 Execute(Pact),
588 /// Settle the claim identified by the retainer with the decided outcome.
589 Settle(Retainer, Outcome),
590 /// Nothing remains to be performed for this step.
591 Idle,
592 }
593
594 /// A report a runtime feeds back after performing a [`Directive`].
595 ///
596 /// `#[non_exhaustive]`: this advanced-tier protocol may gain notices, so a
597 /// consumer's match must carry a wildcard arm.
598 #[derive(Debug, Clone)]
599 #[non_exhaustive]
600 #[must_use]
601 pub enum Notice {
602 /// Result of a claim: a claim if one was available, else none.
603 Claimed(Option<Claim>),
604 /// An execution produced a lifecycle outcome.
605 Executed(Outcome),
606 /// The execution infrastructure failed to run the pact — the executor
607 /// produced no outcome. The kernel fabricates no outcome for this notice: it
608 /// settles nothing and reaches an unsettled terminal, leaving the claim to
609 /// lapse and be reclaimed, while the runtime surfaces the error to its caller.
610 ExecutionFailed,
611 /// A settlement was persisted.
612 Settled,
613 }
614
615 /// The terminal result of one lifecycle step.
616 ///
617 /// `#[non_exhaustive]`: this advanced-tier protocol may gain results, so a
618 /// consumer's match must carry a wildcard arm.
619 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
620 #[non_exhaustive]
621 #[must_use]
622 pub enum StepResult {
623 /// No pact was available to claim.
624 Idle,
625 /// The claim was settled with this outcome.
626 Settled(Outcome),
627 /// Execution produced no outcome (an infrastructure failure), so nothing was
628 /// settled. The claim is left held-but-unsettled to lapse and be reclaimed;
629 /// the kernel fabricates no `Outcome` from the absence of one.
630 Unsettled,
631 }
632
633 #[derive(Debug)]
634 enum Phase {
635 Claiming,
636 Executing {
637 pact: Pact,
638 retainer: Retainer,
639 },
640 Settling {
641 retainer: Retainer,
642 outcome: Outcome,
643 },
644 DoneIdle,
645 DoneSettled(Outcome),
646 DoneUnsettled,
647 }
648
649 /// The pure lifecycle state machine for a single step.
650 #[derive(Debug)]
651 pub struct Kernel {
652 phase: Phase,
653 }
654
655 impl Kernel {
656 /// Start a fresh lifecycle step.
657 #[must_use]
658 pub fn new() -> Self {
659 Self {
660 phase: Phase::Claiming,
661 }
662 }
663
664 /// Decide the next directive from the current state.
665 pub fn poll(&self) -> Directive {
666 match &self.phase {
667 Phase::Claiming => Directive::Claim,
668 Phase::Executing { pact, .. } => Directive::Execute(pact.clone()),
669 Phase::Settling { retainer, outcome } => {
670 Directive::Settle(retainer.clone(), *outcome)
671 }
672 Phase::DoneIdle | Phase::DoneSettled(_) | Phase::DoneUnsettled => Directive::Idle,
673 }
674 }
675
676 /// Absorb a runtime report, advancing the lifecycle.
677 pub fn on_event(&mut self, notice: Notice) {
678 let phase = std::mem::replace(&mut self.phase, Phase::DoneIdle);
679 self.phase = match (phase, notice) {
680 (Phase::Claiming, Notice::Claimed(Some(claim))) => Phase::Executing {
681 pact: claim.pact,
682 retainer: claim.retainer,
683 },
684 (Phase::Claiming, Notice::Claimed(None)) => Phase::DoneIdle,
685 (Phase::Executing { retainer, .. }, Notice::Executed(outcome)) => {
686 Phase::Settling { retainer, outcome }
687 }
688 (Phase::Executing { .. }, Notice::ExecutionFailed) => Phase::DoneUnsettled,
689 (Phase::Settling { outcome, .. }, Notice::Settled) => Phase::DoneSettled(outcome),
690 (other, _) => other,
691 };
692 }
693
694 /// The terminal result once the step reaches a terminal state, else `None`.
695 #[must_use]
696 pub fn result(&self) -> Option<StepResult> {
697 match &self.phase {
698 Phase::DoneIdle => Some(StepResult::Idle),
699 Phase::DoneSettled(outcome) => Some(StepResult::Settled(*outcome)),
700 Phase::DoneUnsettled => Some(StepResult::Unsettled),
701 _ => None,
702 }
703 }
704 }
705
706 impl Default for Kernel {
707 fn default() -> Self {
708 Self::new()
709 }
710 }
711
712 #[cfg(test)]
713 mod tests {
714 use super::*;
715 use crate::Timestamp;
716 use uuid::Uuid;
717
718 fn claim() -> Claim {
719 Claim::new(
720 Pact::new(
721 Uuid::new_v4(),
722 "default".to_string(),
723 "example".to_string(),
724 Vec::new(),
725 ),
726 Retainer::new(Uuid::new_v4()),
727 Timestamp::from_millis(0),
728 )
729 }
730
731 fn drive(
732 kernel: &mut Kernel,
733 execution: Result<Outcome, ()>,
734 claimable: bool,
735 ) -> StepResult {
736 loop {
737 if let Some(result) = kernel.result() {
738 return result;
739 }
740 match kernel.poll() {
741 Directive::Claim => {
742 let notice = if claimable {
743 Notice::Claimed(Some(claim()))
744 } else {
745 Notice::Claimed(None)
746 };
747 kernel.on_event(notice);
748 }
749 Directive::Execute(_) => kernel.on_event(match execution {
750 Ok(outcome) => Notice::Executed(outcome),
751 Err(()) => Notice::ExecutionFailed,
752 }),
753 Directive::Settle(_, _) => kernel.on_event(Notice::Settled),
754 Directive::Idle => return StepResult::Idle,
755 }
756 }
757 }
758
759 #[test]
760 fn fulfilled_execution_settles_fulfilled() {
761 let mut kernel = Kernel::new();
762 assert_eq!(
763 drive(&mut kernel, Ok(Outcome::Fulfilled), true),
764 StepResult::Settled(Outcome::Fulfilled)
765 );
766 }
767
768 #[test]
769 fn breached_execution_settles_breached() {
770 let mut kernel = Kernel::new();
771 assert_eq!(
772 drive(&mut kernel, Ok(Outcome::Breached), true),
773 StepResult::Settled(Outcome::Breached)
774 );
775 }
776
777 #[test]
778 fn infrastructure_error_is_unsettled() {
779 let mut kernel = Kernel::new();
780 assert_eq!(drive(&mut kernel, Err(()), true), StepResult::Unsettled);
781 }
782
783 #[test]
784 fn empty_claim_is_idle() {
785 let mut kernel = Kernel::new();
786 assert_eq!(
787 drive(&mut kernel, Ok(Outcome::Fulfilled), false),
788 StepResult::Idle
789 );
790 }
791 }
792}