Skip to main content

pacta_driver/
lib.rs

1//! Mechanical runtime loop for Pacta execution.
2
3#![forbid(unsafe_code)]
4#![warn(missing_docs)]
5
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use pacta_contract::kernel::{Directive, Kernel, Notice, StepResult};
9use pacta_contract::{Outcome, Registry, Timestamp};
10use pacta_executor::{Execution, Executor};
11
12/// Read the current wall-clock time as a [`Timestamp`] to inject into
13/// time-dependent registry operations. Reading the clock is a runtime concern, so
14/// it lives here and never in the core contract.
15fn current_time() -> Timestamp {
16    let millis = SystemTime::now()
17        .duration_since(UNIX_EPOCH)
18        .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
19        .unwrap_or(0);
20    Timestamp::from_millis(millis)
21}
22
23/// One mechanical driver step result.
24///
25/// `#[non_exhaustive]`: a runtime-loop status may gain states (for example a future
26/// heartbeat or lapse step), so a downstream match must carry a wildcard arm.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Step {
30    /// No pact was available from the configured dockets.
31    Idle,
32    /// A claimed pact was fulfilled.
33    Fulfilled,
34    /// A claimed pact was breached.
35    Breached,
36}
37
38/// Error returned by a driver step.
39///
40/// `#[non_exhaustive]`: an error enumeration grows as new failure modes appear, so a
41/// downstream match must carry a wildcard arm.
42#[derive(Debug, Clone, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum DriverError<RegistryError, ExecutorError> {
45    /// Registry operation failed.
46    Registry(RegistryError),
47    /// Executor infrastructure failed; the claim was left unsettled to lapse and be
48    /// reclaimed (no settlement was recorded).
49    Executor(ExecutorError),
50}
51
52impl<RegistryError, ExecutorError> std::fmt::Display for DriverError<RegistryError, ExecutorError>
53where
54    RegistryError: std::error::Error,
55    ExecutorError: std::error::Error,
56{
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::Registry(error) => write!(f, "registry operation failed: {error}"),
60            Self::Executor(error) => write!(f, "executor infrastructure failed: {error}"),
61        }
62    }
63}
64
65impl<RegistryError, ExecutorError> std::error::Error for DriverError<RegistryError, ExecutorError>
66where
67    RegistryError: std::error::Error + 'static,
68    ExecutorError: std::error::Error + 'static,
69{
70    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71        match self {
72            Self::Registry(error) => Some(error),
73            Self::Executor(error) => Some(error),
74        }
75    }
76}
77
78/// Mechanical loop that performs the directives the sans-I/O kernel issues.
79///
80/// This is a **reference** runtime skeleton. It drives one step synchronously —
81/// claim, execute, settle — and never heartbeats or reclaims within a step: it does
82/// not extend a lease while its executor runs (so a long-running pact's lease can
83/// *expire* mid-step), and it settles by matching the retainer rather than
84/// re-claiming. It is therefore safe for pacts shorter than the lease (the lease
85/// never expires mid-step)
86/// and for single-worker use (no concurrent claimer can *reclaim* an expired lease
87/// mid-step). A workload that is both long-running *and* multi-worker should compose
88/// its own loop over the [`Registry`] contract (which includes `heartbeat`); the
89/// lifecycle kernel deliberately models no in-flight heartbeat.
90pub struct Driver<R, E> {
91    registry: R,
92    executor: E,
93    dockets: Vec<String>,
94}
95
96impl<R, E> Driver<R, E> {
97    /// Build a driver from a registry, an executor, and docket names.
98    pub fn new(registry: R, executor: E, dockets: impl IntoIterator<Item = String>) -> Self {
99        Self {
100            registry,
101            executor,
102            dockets: dockets.into_iter().collect(),
103        }
104    }
105
106    /// Borrow the registry used by this driver.
107    #[must_use]
108    pub fn registry(&self) -> &R {
109        &self.registry
110    }
111
112    /// Borrow the executor used by this driver.
113    #[must_use]
114    pub fn executor(&self) -> &E {
115        &self.executor
116    }
117}
118
119impl<R, E> Driver<R, E>
120where
121    R: Registry,
122    E: Executor,
123{
124    /// Perform one claim, execute, and settle step by driving the kernel: the
125    /// kernel decides each directive; the driver performs it and feeds a notice
126    /// back, deciding no lifecycle outcome itself.
127    pub fn step(&mut self) -> Result<Step, DriverError<R::Error, E::Error>> {
128        let dockets: Vec<&str> = self.dockets.iter().map(String::as_str).collect();
129        let now = current_time();
130        let mut kernel = Kernel::new();
131        let mut pending_executor_error: Option<E::Error> = None;
132
133        loop {
134            if let Some(result) = kernel.result() {
135                return match result {
136                    StepResult::Idle => Ok(Step::Idle),
137                    StepResult::Settled(outcome) => Ok(match outcome {
138                        Outcome::Fulfilled => Step::Fulfilled,
139                        Outcome::Breached => Step::Breached,
140                    }),
141                    // An unsettled step means execution failed: settle nothing and
142                    // surface the executor error. The claim lapses and is reclaimed.
143                    StepResult::Unsettled => Err(DriverError::Executor(
144                        pending_executor_error
145                            .expect("an unsettled step implies a pending executor error"),
146                    )),
147                    _ => unreachable!("driver handles every current kernel step result"),
148                };
149            }
150
151            match kernel.poll() {
152                Directive::Claim => {
153                    let claim = self
154                        .registry
155                        .claim(&dockets, now)
156                        .map_err(DriverError::Registry)?;
157                    kernel.on_event(Notice::Claimed(claim));
158                }
159                Directive::Execute(pact) => match self.executor.execute(Execution::new(pact)) {
160                    Ok(outcome) => kernel.on_event(Notice::Executed(outcome)),
161                    Err(error) => {
162                        pending_executor_error = Some(error);
163                        kernel.on_event(Notice::ExecutionFailed);
164                    }
165                },
166                Directive::Settle(retainer, outcome) => {
167                    match outcome {
168                        Outcome::Fulfilled => self
169                            .registry
170                            .fulfill(&retainer)
171                            .map_err(DriverError::Registry)?,
172                        Outcome::Breached => self
173                            .registry
174                            .breach(&retainer)
175                            .map_err(DriverError::Registry)?,
176                    }
177                    kernel.on_event(Notice::Settled);
178                }
179                Directive::Idle => return Ok(Step::Idle),
180                _ => unreachable!("driver handles every current kernel directive"),
181            }
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use std::sync::Mutex;
189
190    use pacta_contract::{Claim, Pact, Retainer, Timestamp, Transition};
191    use uuid::Uuid;
192
193    use super::*;
194
195    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
196    struct TestError;
197
198    impl std::fmt::Display for TestError {
199        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200            write!(f, "test error")
201        }
202    }
203
204    impl std::error::Error for TestError {}
205
206    #[derive(Default)]
207    struct RegistryState {
208        claim: Option<Claim>,
209        claimed_dockets: Vec<Vec<String>>,
210        fulfilled: usize,
211        breached: usize,
212    }
213
214    #[derive(Default)]
215    struct TestRegistry {
216        state: Mutex<RegistryState>,
217    }
218
219    impl TestRegistry {
220        fn with_claim(claim: Claim) -> Self {
221            Self {
222                state: Mutex::new(RegistryState {
223                    claim: Some(claim),
224                    ..RegistryState::default()
225                }),
226            }
227        }
228    }
229
230    impl Registry for TestRegistry {
231        type Error = TestError;
232
233        fn claim(&self, dockets: &[&str], _now: Timestamp) -> Result<Option<Claim>, Self::Error> {
234            self.state
235                .lock()
236                .expect("registry state should not be poisoned")
237                .claimed_dockets
238                .push(dockets.iter().map(ToString::to_string).collect());
239            Ok(self
240                .state
241                .lock()
242                .expect("registry state should not be poisoned")
243                .claim
244                .take())
245        }
246
247        fn lease_millis(&self) -> u64 {
248            0
249        }
250
251        // Required port; unused by these tests (the driver settles via fulfill/breach, which this
252        // registry overrides below to observe the driver's routing).
253        fn apply(
254            &self,
255            _retainer: &Retainer,
256            _transition: &Transition<'_>,
257        ) -> Result<(), Self::Error> {
258            Ok(())
259        }
260
261        // fulfill and breach are overridden (not to store state, but to count which the driver
262        // called), so the tests can assert the driver routes each outcome to the right op.
263        fn fulfill(&self, _retainer: &Retainer) -> Result<(), Self::Error> {
264            self.state
265                .lock()
266                .expect("registry state should not be poisoned")
267                .fulfilled += 1;
268            Ok(())
269        }
270
271        fn breach(&self, _retainer: &Retainer) -> Result<(), Self::Error> {
272            self.state
273                .lock()
274                .expect("registry state should not be poisoned")
275                .breached += 1;
276            Ok(())
277        }
278    }
279
280    struct TestExecutor {
281        outcome: Result<Outcome, TestError>,
282        executions: usize,
283    }
284
285    impl Executor for TestExecutor {
286        type Error = TestError;
287
288        fn execute(&mut self, _execution: Execution) -> Result<Outcome, Self::Error> {
289            self.executions += 1;
290            self.outcome
291        }
292    }
293
294    fn claim() -> Claim {
295        Claim::new(
296            Pact::new(
297                Uuid::new_v4(),
298                "default".to_string(),
299                "example".to_string(),
300                Vec::new(),
301            ),
302            Retainer::new(Uuid::new_v4()),
303            Timestamp::from_millis(0),
304        )
305    }
306
307    #[test]
308    fn successful_execution_fulfills_claim() {
309        let registry = TestRegistry::with_claim(claim());
310        let executor = TestExecutor {
311            outcome: Ok(Outcome::Fulfilled),
312            executions: 0,
313        };
314        let mut driver = Driver::new(registry, executor, ["default".to_string()]);
315
316        assert_eq!(driver.step(), Ok(Step::Fulfilled));
317        let state = driver
318            .registry()
319            .state
320            .lock()
321            .expect("registry state should not be poisoned");
322        assert_eq!(state.fulfilled, 1);
323        assert_eq!(state.breached, 0);
324        drop(state);
325        assert_eq!(driver.executor().executions, 1);
326    }
327
328    #[test]
329    fn breached_execution_breaches_claim() {
330        let registry = TestRegistry::with_claim(claim());
331        let executor = TestExecutor {
332            outcome: Ok(Outcome::Breached),
333            executions: 0,
334        };
335        let mut driver = Driver::new(registry, executor, ["default".to_string()]);
336
337        assert_eq!(driver.step(), Ok(Step::Breached));
338        let state = driver
339            .registry()
340            .state
341            .lock()
342            .expect("registry state should not be poisoned");
343        assert_eq!(state.fulfilled, 0);
344        assert_eq!(state.breached, 1);
345        drop(state);
346        assert_eq!(driver.executor().executions, 1);
347    }
348
349    #[test]
350    fn executor_error_leaves_claim_unsettled() {
351        let registry = TestRegistry::with_claim(claim());
352        let executor = TestExecutor {
353            outcome: Err(TestError),
354            executions: 0,
355        };
356        let mut driver = Driver::new(registry, executor, ["default".to_string()]);
357
358        // An infrastructure failure surfaces the executor error and settles nothing:
359        // neither fulfilled nor breached. The claim is left unsettled to lapse and be
360        // reclaimed — that lapse-reclaim is proven at the registry level by
361        // `pacta-conformance` (`expired_lease_lapses_and_reclaims_...`).
362        assert_eq!(driver.step(), Err(DriverError::Executor(TestError)));
363        let state = driver
364            .registry()
365            .state
366            .lock()
367            .expect("registry state should not be poisoned");
368        assert_eq!(state.fulfilled, 0);
369        assert_eq!(state.breached, 0);
370        drop(state);
371        assert_eq!(driver.executor().executions, 1);
372    }
373
374    #[test]
375    fn empty_docket_is_idle() {
376        let registry = TestRegistry::default();
377        let executor = TestExecutor {
378            outcome: Ok(Outcome::Fulfilled),
379            executions: 0,
380        };
381        let mut driver = Driver::new(registry, executor, ["default".to_string()]);
382
383        assert_eq!(driver.step(), Ok(Step::Idle));
384        let state = driver
385            .registry()
386            .state
387            .lock()
388            .expect("registry state should not be poisoned");
389        assert_eq!(state.fulfilled, 0);
390        assert_eq!(state.breached, 0);
391        drop(state);
392        assert_eq!(driver.executor().executions, 0);
393    }
394
395    #[test]
396    fn driver_error_displays_and_exposes_source() {
397        use std::error::Error;
398
399        let executor_error: DriverError<TestError, TestError> = DriverError::Executor(TestError);
400        assert_eq!(
401            executor_error.to_string(),
402            "executor infrastructure failed: test error"
403        );
404        assert_eq!(
405            executor_error
406                .source()
407                .expect("driver error should expose its source")
408                .to_string(),
409            "test error"
410        );
411
412        let registry_error: DriverError<TestError, TestError> = DriverError::Registry(TestError);
413        assert_eq!(
414            registry_error.to_string(),
415            "registry operation failed: test error"
416        );
417        assert_eq!(
418            registry_error
419                .source()
420                .expect("driver error should expose its source")
421                .to_string(),
422            "test error"
423        );
424    }
425}