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