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