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