Skip to main content

sim_lib_operation_gate/
lifecycle.rs

1//! Reconciled M5 operation lifecycle over the canonical journal.
2
3use std::fmt;
4
5use sim_kernel::{ContentId, Datum, Symbol};
6
7use crate::{
8    OperationError, OperationId,
9    lifecycle_wire::{
10        fenced_dispatch_datum, lease_datum, lifecycle_receipt_datum, observation_base_datum,
11        observation_datum, optional_id, optional_id_datum, response_datum, response_from_datum,
12        u64_datum,
13    },
14    operation_wire::{content_id, field, id_datum, id_from_datum, node, node_fields, u64_field},
15};
16
17pub(super) const LEASE_TAG: &str = "bounded-lease-v1";
18pub(super) const DISPATCH_TAG: &str = "fenced-dispatch-v1";
19pub(super) const RECEIPT_TAG: &str = "lifecycle-receipt-v1";
20pub(super) const OBSERVATION_TAG: &str = "postcondition-observation-v1";
21pub(super) const OUTCOME_TAG: &str = "outcome-v1";
22
23macro_rules! semantic_id {
24    ($name:ident, $doc:literal) => {
25        #[doc = $doc]
26        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
27        pub struct $name(pub(super) ContentId);
28
29        impl $name {
30            /// Borrows the semantic content identity.
31            pub const fn content_id(&self) -> &ContentId {
32                &self.0
33            }
34        }
35
36        impl fmt::Display for $name {
37            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38                crate::operation_wire::render_id(&self.0, formatter)
39            }
40        }
41    };
42}
43
44semantic_id!(
45    OperationLeaseId,
46    "Identity of one bounded, fenced operation lease."
47);
48semantic_id!(
49    FencedDispatchId,
50    "Identity of one lease-bound durable dispatch."
51);
52semantic_id!(
53    LifecycleReceiptId,
54    "Identity of one raw lifecycle performer receipt."
55);
56semantic_id!(
57    OperationObservationId,
58    "Identity of one independent postcondition observation."
59);
60semantic_id!(
61    EvidenceSetId,
62    "Identity of the evidence carried by one observation."
63);
64semantic_id!(
65    OperationOutcomeId,
66    "Identity of one durable reconciliation outcome."
67);
68
69/// Explicit holder and monotonic bounds for one operation lease acquisition.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct LeaseWindow {
72    pub(super) holder: Datum,
73    pub(super) acquired_at: u64,
74    pub(super) expires_at: u64,
75}
76
77impl LeaseWindow {
78    /// Validates a non-empty half-open monotonic lease interval.
79    pub fn new(holder: Datum, acquired_at: u64, expires_at: u64) -> Result<Self, OperationError> {
80        if expires_at <= acquired_at {
81            return Err(OperationError::InvalidLease);
82        }
83        Ok(Self {
84            holder,
85            acquired_at,
86            expires_at,
87        })
88    }
89
90    /// Returns the explicit holder identity.
91    pub const fn holder(&self) -> &Datum {
92        &self.holder
93    }
94
95    /// Returns the inclusive monotonic acquisition tick.
96    pub const fn acquired_at(&self) -> u64 {
97        self.acquired_at
98    }
99
100    /// Returns the exclusive monotonic expiry tick.
101    pub const fn expires_at(&self) -> u64 {
102        self.expires_at
103    }
104}
105
106/// Bounded effect authority tied to the journal writer fence that recorded it.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct OperationLease {
109    pub(super) id: OperationLeaseId,
110    pub(super) operation: OperationId,
111    pub(super) holder: Datum,
112    pub(super) fence: u64,
113    pub(super) acquired_at: u64,
114    pub(super) expires_at: u64,
115}
116
117impl OperationLease {
118    pub(super) fn new(
119        operation: OperationId,
120        holder: Datum,
121        fence: u64,
122        acquired_at: u64,
123        expires_at: u64,
124    ) -> Result<Self, OperationError> {
125        if expires_at <= acquired_at {
126            return Err(OperationError::InvalidLease);
127        }
128        let datum = lease_datum(&operation, &holder, fence, acquired_at, expires_at);
129        Ok(Self {
130            id: OperationLeaseId(content_id(&datum)?),
131            operation,
132            holder,
133            fence,
134            acquired_at,
135            expires_at,
136        })
137    }
138
139    /// Returns the semantic lease identity.
140    pub const fn id(&self) -> &OperationLeaseId {
141        &self.id
142    }
143    /// Returns the stable operation protected by the lease.
144    pub const fn operation(&self) -> &OperationId {
145        &self.operation
146    }
147    /// Returns the explicit holder identity.
148    pub const fn holder(&self) -> &Datum {
149        &self.holder
150    }
151    /// Returns the journal writer fence bound into the lease.
152    pub const fn fence(&self) -> u64 {
153        self.fence
154    }
155    /// Returns the caller-supplied monotonic acquisition tick.
156    pub const fn acquired_at(&self) -> u64 {
157        self.acquired_at
158    }
159    /// Returns the exclusive caller-supplied monotonic expiry tick.
160    pub const fn expires_at(&self) -> u64 {
161        self.expires_at
162    }
163    /// Returns true while the explicit monotonic time remains inside the bound.
164    pub const fn is_live_at(&self, now: u64) -> bool {
165        self.acquired_at <= now && now < self.expires_at
166    }
167    /// Returns the canonical semantic lease value.
168    pub fn canonical_datum(&self) -> Datum {
169        lease_datum(
170            &self.operation,
171            &self.holder,
172            self.fence,
173            self.acquired_at,
174            self.expires_at,
175        )
176    }
177    pub(super) fn from_datum(datum: &Datum) -> Result<Self, OperationError> {
178        let fields = node_fields(datum, LEASE_TAG, 5)?;
179        let operation = OperationId(id_from_datum(field(fields, "operation")?)?);
180        let holder = field(fields, "holder")?.clone();
181        let value = Self::new(
182            operation,
183            holder,
184            u64_field(fields, "fence")?,
185            u64_field(fields, "acquired-at")?,
186            u64_field(fields, "expires-at")?,
187        )?;
188        if value.canonical_datum() != *datum {
189            return Err(OperationError::NonCanonical("operation lease"));
190        }
191        Ok(value)
192    }
193}
194
195/// Durable performer handoff bound to an exact grant, attempt, and fenced lease.
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct FencedDispatch {
198    pub(super) id: FencedDispatchId,
199    pub(super) operation: OperationId,
200    pub(super) grant: crate::OperationGrantId,
201    pub(super) attempt: crate::OperationAttemptId,
202    pub(super) lease: OperationLeaseId,
203    pub(super) performer: Datum,
204}
205
206impl FencedDispatch {
207    pub(super) fn new(
208        operation: OperationId,
209        grant: crate::OperationGrantId,
210        attempt: crate::OperationAttemptId,
211        lease: OperationLeaseId,
212        performer: Datum,
213    ) -> Result<Self, OperationError> {
214        let datum = fenced_dispatch_datum(&operation, &grant, &attempt, &lease, &performer);
215        Ok(Self {
216            id: FencedDispatchId(content_id(&datum)?),
217            operation,
218            grant,
219            attempt,
220            lease,
221            performer,
222        })
223    }
224    /// Returns the dispatch identity supplied as the performer idempotency token.
225    pub const fn id(&self) -> &FencedDispatchId {
226        &self.id
227    }
228    /// Returns the stable semantic operation identity.
229    pub const fn operation(&self) -> &OperationId {
230        &self.operation
231    }
232    /// Returns the separately persisted least-authority grant identity.
233    pub const fn grant(&self) -> &crate::OperationGrantId {
234        &self.grant
235    }
236    /// Returns the separately persisted attempt identity.
237    pub const fn attempt(&self) -> &crate::OperationAttemptId {
238        &self.attempt
239    }
240    /// Returns the bounded lease authorizing this dispatch.
241    pub const fn lease(&self) -> &OperationLeaseId {
242        &self.lease
243    }
244    /// Returns the exact performer authority selected for this handoff.
245    pub const fn performer(&self) -> &Datum {
246        &self.performer
247    }
248    /// Returns the canonical semantic dispatch value.
249    pub fn canonical_datum(&self) -> Datum {
250        fenced_dispatch_datum(
251            &self.operation,
252            &self.grant,
253            &self.attempt,
254            &self.lease,
255            &self.performer,
256        )
257    }
258    pub(super) fn from_datum(datum: &Datum) -> Result<Self, OperationError> {
259        let fields = node_fields(datum, DISPATCH_TAG, 5)?;
260        let value = Self::new(
261            OperationId(id_from_datum(field(fields, "operation")?)?),
262            crate::OperationGrantId(id_from_datum(field(fields, "grant")?)?),
263            crate::OperationAttemptId(id_from_datum(field(fields, "attempt")?)?),
264            OperationLeaseId(id_from_datum(field(fields, "lease")?)?),
265            field(fields, "performer")?.clone(),
266        )?;
267        if value.canonical_datum() != *datum {
268            return Err(OperationError::NonCanonical("fenced dispatch"));
269        }
270        Ok(value)
271    }
272}
273
274/// Raw performer acknowledgement bound to one fenced dispatch.
275#[derive(Clone, Debug, PartialEq, Eq)]
276pub struct LifecycleReceipt {
277    pub(super) id: LifecycleReceiptId,
278    pub(super) dispatch: FencedDispatchId,
279    pub(super) raw: Datum,
280}
281
282impl LifecycleReceipt {
283    pub(super) fn new(dispatch: FencedDispatchId, raw: Datum) -> Result<Self, OperationError> {
284        let datum = lifecycle_receipt_datum(&dispatch, &raw);
285        Ok(Self {
286            id: LifecycleReceiptId(content_id(&datum)?),
287            dispatch,
288            raw,
289        })
290    }
291    /// Returns the semantic receipt identity.
292    pub const fn id(&self) -> &LifecycleReceiptId {
293        &self.id
294    }
295    /// Returns the acknowledged dispatch.
296    pub const fn dispatch(&self) -> &FencedDispatchId {
297        &self.dispatch
298    }
299    /// Returns the uninterpreted raw acknowledgement.
300    pub const fn raw(&self) -> &Datum {
301        &self.raw
302    }
303    /// Returns the canonical receipt value.
304    pub fn canonical_datum(&self) -> Datum {
305        lifecycle_receipt_datum(&self.dispatch, &self.raw)
306    }
307    pub(super) fn from_datum(datum: &Datum) -> Result<Self, OperationError> {
308        let fields = node_fields(datum, RECEIPT_TAG, 2)?;
309        let value = Self::new(
310            FencedDispatchId(id_from_datum(field(fields, "dispatch")?)?),
311            field(fields, "raw")?.clone(),
312        )?;
313        if value.canonical_datum() != *datum {
314            return Err(OperationError::NonCanonical("lifecycle receipt"));
315        }
316        Ok(value)
317    }
318}
319
320/// Last durable lifecycle boundary available to reconciliation.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub enum OperationStep {
323    /// Intent and grant are durable.
324    IntentPersisted,
325    /// A bounded, fenced operation lease is durable.
326    LeaseAcquired,
327    /// The performer handoff is durable.
328    DispatchPersisted,
329    /// The raw performer acknowledgement is durable.
330    ReceiptPersisted,
331    /// Independent postcondition evidence is durable.
332    ObservationPersisted,
333    /// A reconciliation outcome is durable.
334    OutcomePersisted,
335}
336
337impl OperationStep {
338    pub(super) fn datum(self) -> Datum {
339        Datum::Symbol(Symbol::qualified(
340            "operation-step",
341            match self {
342                Self::IntentPersisted => "intent-persisted",
343                Self::LeaseAcquired => "lease-acquired",
344                Self::DispatchPersisted => "dispatch-persisted",
345                Self::ReceiptPersisted => "receipt-persisted",
346                Self::ObservationPersisted => "observation-persisted",
347                Self::OutcomePersisted => "outcome-persisted",
348            },
349        ))
350    }
351    pub(super) fn from_datum(datum: &Datum) -> Result<Self, OperationError> {
352        let Datum::Symbol(value) = datum else {
353            return Err(OperationError::NonCanonical("operation step"));
354        };
355        for step in [
356            Self::IntentPersisted,
357            Self::LeaseAcquired,
358            Self::DispatchPersisted,
359            Self::ReceiptPersisted,
360            Self::ObservationPersisted,
361            Self::OutcomePersisted,
362        ] {
363            if step.datum() == Datum::Symbol(value.clone()) {
364                return Ok(step);
365            }
366        }
367        Err(OperationError::NonCanonical("operation step"))
368    }
369}
370
371/// Request passed to an independent observer without performer authority.
372#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct PostconditionRequest {
374    pub(super) operation: OperationId,
375    pub(super) target: Datum,
376    pub(super) expected: Datum,
377    pub(super) dispatch: Option<FencedDispatchId>,
378    pub(super) receipt: Option<LifecycleReceiptId>,
379    pub(super) last_durable_step: OperationStep,
380    pub(super) observed_at: u64,
381}
382
383impl PostconditionRequest {
384    /// Returns the semantic operation identity.
385    pub const fn operation(&self) -> &OperationId {
386        &self.operation
387    }
388    /// Returns the exact target to inspect.
389    pub const fn target(&self) -> &Datum {
390        &self.target
391    }
392    /// Returns the expected semantic postcondition.
393    pub const fn expected(&self) -> &Datum {
394        &self.expected
395    }
396    /// Returns the latest dispatch, when performance was durably authorized.
397    pub const fn dispatch(&self) -> Option<&FencedDispatchId> {
398        self.dispatch.as_ref()
399    }
400    /// Returns the latest raw receipt identity, when one became durable.
401    pub const fn receipt(&self) -> Option<&LifecycleReceiptId> {
402        self.receipt.as_ref()
403    }
404    /// Returns the last durable lifecycle boundary known before observation.
405    pub const fn last_durable_step(&self) -> OperationStep {
406        self.last_durable_step
407    }
408    /// Returns the caller-supplied monotonic tick at which observation began.
409    pub const fn observed_at(&self) -> u64 {
410        self.observed_at
411    }
412}
413
414/// Typed result from a postcondition observer.
415#[derive(Clone, Debug, PartialEq, Eq)]
416pub enum PostconditionResponse {
417    /// The intended postcondition is present.
418    Satisfied {
419        /// Exact semantic value observed.
420        observed: Datum,
421        /// Observer-owned evidence describing the read.
422        evidence: Datum,
423    },
424    /// The observer proved that the intended postcondition is absent.
425    NotSatisfied {
426        /// Exact semantic value observed instead.
427        observed: Datum,
428        /// Observer-owned evidence describing the read.
429        evidence: Datum,
430    },
431    /// The observer could not establish a value.
432    Unavailable {
433        /// Typed reason no trustworthy value could be obtained.
434        reason: Datum,
435    },
436    /// Independent observation sources disagreed.
437    Disputed {
438        /// First incompatible observation.
439        first: Datum,
440        /// Second incompatible observation.
441        second: Datum,
442    },
443}
444
445/// Effect-free identity plus independently performed postcondition observation.
446pub trait PostconditionObserver {
447    /// Returns the stable observer authority identity.
448    fn identity(&self) -> Datum;
449    /// Observes the postcondition without performing the requested operation.
450    fn observe(&mut self, request: &PostconditionRequest) -> PostconditionResponse;
451}
452
453/// Result of calling a lifecycle performer after durable dispatch.
454#[derive(Clone, Debug, PartialEq, Eq)]
455pub enum LifecyclePerformerResponse {
456    /// The performer returned an uninterpreted raw acknowledgement.
457    Receipt(Datum),
458    /// Performance may have occurred, but its acknowledgement was lost.
459    AcknowledgementMissing,
460}
461
462/// Effect authority invoked only after a matching fenced dispatch is durable.
463pub trait LifecyclePerformer {
464    /// Returns the stable performer authority identity.
465    fn identity(&self) -> Datum;
466    /// Performs the exact durable dispatch once.
467    fn perform(&mut self, dispatch: &FencedDispatch) -> LifecyclePerformerResponse;
468}
469
470/// Durable independent observation of one operation postcondition.
471#[derive(Clone, Debug, PartialEq, Eq)]
472pub struct OperationObservation {
473    pub(super) id: OperationObservationId,
474    pub(super) operation: OperationId,
475    pub(super) observer: Datum,
476    pub(super) response: PostconditionResponse,
477    pub(super) dispatch: Option<FencedDispatchId>,
478    pub(super) receipt: Option<LifecycleReceiptId>,
479    pub(super) last_durable_step: OperationStep,
480    pub(super) observed_at: u64,
481    pub(super) evidence: EvidenceSetId,
482}
483
484impl OperationObservation {
485    pub(super) fn new(
486        request: &PostconditionRequest,
487        observer: Datum,
488        response: PostconditionResponse,
489    ) -> Result<Self, OperationError> {
490        let base = observation_base_datum(request, &observer, &response);
491        let evidence = EvidenceSetId(content_id(&node(
492            "evidence-set-v1",
493            vec![("observation", base.clone())],
494        ))?);
495        let datum = observation_datum(request, &observer, &response, &evidence);
496        Ok(Self {
497            id: OperationObservationId(content_id(&datum)?),
498            operation: request.operation.clone(),
499            observer,
500            response,
501            dispatch: request.dispatch.clone(),
502            receipt: request.receipt.clone(),
503            last_durable_step: request.last_durable_step,
504            observed_at: request.observed_at,
505            evidence,
506        })
507    }
508    /// Returns the semantic observation identity.
509    pub const fn id(&self) -> &OperationObservationId {
510        &self.id
511    }
512    /// Returns the stable operation observed.
513    pub const fn operation(&self) -> &OperationId {
514        &self.operation
515    }
516    /// Returns the independent observer identity.
517    pub const fn observer(&self) -> &Datum {
518        &self.observer
519    }
520    /// Returns the typed observation response.
521    pub const fn response(&self) -> &PostconditionResponse {
522        &self.response
523    }
524    /// Returns the dispatch observed, if any.
525    pub const fn dispatch(&self) -> Option<&FencedDispatchId> {
526        self.dispatch.as_ref()
527    }
528    /// Returns the latest raw receipt visible to the observer, if any.
529    pub const fn receipt(&self) -> Option<&LifecycleReceiptId> {
530        self.receipt.as_ref()
531    }
532    /// Returns the last durable step visible to the observer.
533    pub const fn last_durable_step(&self) -> OperationStep {
534        self.last_durable_step
535    }
536    /// Returns the explicit monotonic observation tick.
537    pub const fn observed_at(&self) -> u64 {
538        self.observed_at
539    }
540    /// Returns the evidence-set identity derived from the observation.
541    pub const fn evidence(&self) -> &EvidenceSetId {
542        &self.evidence
543    }
544    /// Returns the canonical semantic observation value.
545    pub fn canonical_datum(&self) -> Datum {
546        self.stored_datum()
547    }
548    pub(super) fn from_datum(datum: &Datum) -> Result<Self, OperationError> {
549        let fields = node_fields(datum, OBSERVATION_TAG, 8)?;
550        let operation = OperationId(id_from_datum(field(fields, "operation")?)?);
551        let observer = field(fields, "observer")?.clone();
552        let response = response_from_datum(field(fields, "response")?)?;
553        let dispatch = optional_id(field(fields, "dispatch")?)?.map(FencedDispatchId);
554        let last_durable_step = OperationStep::from_datum(field(fields, "last-durable-step")?)?;
555        let observed_at = u64_field(fields, "observed-at")?;
556        let receipt = optional_id(field(fields, "receipt")?)?.map(LifecycleReceiptId);
557        let evidence = EvidenceSetId(id_from_datum(field(fields, "evidence")?)?);
558        let evidence_input = node(
559            "observation-evidence-v1",
560            vec![
561                ("operation", id_datum(operation.content_id())),
562                ("observer", observer.clone()),
563                ("response", response_datum(&response)),
564                (
565                    "dispatch",
566                    optional_id_datum(dispatch.as_ref().map(FencedDispatchId::content_id)),
567                ),
568                (
569                    "receipt",
570                    optional_id_datum(receipt.as_ref().map(LifecycleReceiptId::content_id)),
571                ),
572                ("last-durable-step", last_durable_step.datum()),
573                ("observed-at", u64_datum(observed_at)),
574            ],
575        );
576        let expected_evidence = EvidenceSetId(content_id(&node(
577            "evidence-set-v1",
578            vec![("observation", evidence_input)],
579        ))?);
580        if evidence != expected_evidence {
581            return Err(OperationError::NonCanonical("observation evidence set"));
582        }
583        let id = OperationObservationId(content_id(datum)?);
584        let value = Self {
585            id,
586            operation,
587            observer,
588            response,
589            dispatch,
590            receipt,
591            last_durable_step,
592            observed_at,
593            evidence,
594        };
595        if value.stored_datum() != *datum {
596            return Err(OperationError::NonCanonical("operation observation"));
597        }
598        Ok(value)
599    }
600    pub(super) fn stored_datum(&self) -> Datum {
601        node(
602            OBSERVATION_TAG,
603            vec![
604                ("operation", id_datum(self.operation.content_id())),
605                ("observer", self.observer.clone()),
606                ("response", response_datum(&self.response)),
607                (
608                    "dispatch",
609                    optional_id_datum(self.dispatch.as_ref().map(FencedDispatchId::content_id)),
610                ),
611                (
612                    "receipt",
613                    optional_id_datum(self.receipt.as_ref().map(LifecycleReceiptId::content_id)),
614                ),
615                ("last-durable-step", self.last_durable_step.datum()),
616                ("observed-at", u64_datum(self.observed_at)),
617                ("evidence", id_datum(self.evidence.content_id())),
618            ],
619        )
620    }
621}
622
623/// Reconciled semantic operation result.
624#[derive(Clone, Debug, PartialEq, Eq)]
625pub enum OperationOutcome {
626    /// Observation proved the intended state before any dispatch.
627    AlreadyTrue {
628        /// Independent evidence proving the pre-existing postcondition.
629        evidence: EvidenceSetId,
630    },
631    /// Observation proved the intended state after a durable dispatch.
632    Verified {
633        /// Independent evidence proving the post-dispatch postcondition.
634        evidence: EvidenceSetId,
635    },
636    /// Observation proved a different state.
637    Diverged {
638        /// Exact independently observed value.
639        observed: Datum,
640        /// Exact intended value.
641        expected: Datum,
642    },
643    /// Available evidence cannot establish completion or safe replay.
644    Uncertain {
645        /// Last lifecycle fact known durably.
646        last_durable_step: OperationStep,
647    },
648}