Skip to main content

rustev_runtime/
lib.rs

1//! Rustev's runtime (spec 003).
2//!
3//! Drives the pure core's staged evaluation through [`DecisionBackend`]s
4//! under explicit bounds: a monotonic [`Clock`] and one end-to-end deadline,
5//! bounded admission and concurrency, cost [`Ledger`]s that reserve before
6//! dispatch, retries and fallback only as the plan's execution policy
7//! declares them, cancellation that records exactly what the backend
8//! acknowledged, and a run record delivered under a [`SinkPolicy`].
9//!
10//! The runtime spawns no task per decision, request or attempt: a decision
11//! runs on the caller's task. The one spawned task is the delivery worker of
12//! the queued sink policies (spec 003, 3.4.3).
13#![forbid(unsafe_code)]
14
15mod acquisition;
16mod capture;
17pub mod clock;
18mod driver;
19mod dynamic;
20pub mod ledger;
21mod live;
22mod optimization;
23mod progress;
24mod sink;
25pub use progress::{ProgressObserver, StepProgress};
26mod wait;
27
28use std::collections::{BTreeMap, BTreeSet};
29use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
30use std::sync::{Arc, Mutex};
31
32use rustev_contract::execution::{
33    AttemptTimeout, CostPolicy, FailureClass, RetryPolicy, StepExecution,
34};
35use rustev_contract::ids::ArtifactId;
36use rustev_contract::judgment::Judgment;
37use rustev_contract::optimization::OptimizationPolicy;
38use rustev_contract::plan::{PlanExecution, PlanStepDetail};
39use rustev_contract::replay::Capture;
40use rustev_contract::run::{
41    CoreEvidence, CostMode, CostModel, MAX_REMOTE_EVIDENCE, RecordedExecution, RemoteEvidence,
42    RequestRecord, RunRecord, Termination, Timing,
43};
44use rustev_contract::snapshot::Snapshot;
45use rustev_contract::time::Timestamp;
46use rustev_contract::{Identified, schema};
47use rustev_core::Compiled;
48use rustev_core::evaluate::Evaluation;
49use rustev_core::seams::{CancelSignal, DecisionBackend, EvidenceSink};
50use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
51
52pub use capture::{CaptureConfig, CaptureConfigError, MAX_CAPTURE_BYTES};
53pub use clock::{Clock, Instant, ManualClock, TokioClock};
54/// Dynamic questions (spec 019): the only execution entry point.
55pub use dynamic::{DynamicAsked, DynamicBounds, DynamicDelivery, ask_dynamic};
56pub use ledger::Ledger;
57pub use live::{DispatchAnswer, LiveAuthorizer, LiveRefusal, LiveRequest, LiveResult};
58pub use optimization::InvalidationResult;
59pub use sink::{DeliveryFailure, DeliveryTicket, SinkPolicy, SinkStats};
60
61use crate::optimization::InvalidationSelector;
62use crate::sink::{Job, Stats};
63use crate::wait::{Raced, race};
64
65/// Bounds of one runtime (spec 003, 3.4).
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct RuntimeConfig {
68    /// Decisions admitted at once.
69    pub max_in_flight: usize,
70    /// Decisions waiting for admission.
71    pub max_queued: usize,
72    /// Requests in progress within one decision.
73    pub max_parallel_requests: usize,
74    pub sink: SinkPolicy,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ConfigError {
79    /// A bound that must be positive is 0.
80    Zero(&'static str),
81    DuplicateBackend(String),
82    InvalidSource(String),
83}
84
85/// Why a plan cannot run on this runtime (spec 003, 3.5.3 and 3.6.5).
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum PrepareError {
88    SourceBinding(String),
89    /// The plan binds a backend id that is not registered.
90    MissingBackend(String),
91    /// The registered backend's descriptor is not the one the plan bound.
92    DescriptorMismatch {
93        backend_id: String,
94    },
95    /// A hard budget, and a reachable backend without enforceable bounds.
96    HardBudgetUnenforceable {
97        backend_id: String,
98        model: CostModel,
99    },
100    /// A plan 3 and an evidence sink that does not accept `rustev.run/3`
101    /// (spec 032, 3.11.2).
102    SinkVersionUnsupported {
103        schema: &'static str,
104    },
105}
106
107/// Why a decision was not admitted (spec 003, 3.4.1). Nothing was dispatched
108/// or spent, and there is no run record.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum Rejection {
111    Overloaded,
112    QueueDeadline,
113    Cancelled,
114    InvalidRequest(String),
115}
116
117#[derive(Debug)]
118pub enum DecideError {
119    Rejected(Rejection),
120    /// Work and spending happened; the record did not reach the sink. The
121    /// judgment inside `record` is evidence, not a result to act on.
122    EvidenceNotDelivered {
123        record: Arc<RunRecord>,
124        failure: DeliveryFailure,
125    },
126}
127
128/// One decision to run.
129#[derive(Debug, Clone)]
130pub struct DecisionRequest {
131    /// Non-empty, at most 256 bytes; the delivery key.
132    pub decision_id: String,
133    pub snapshot: Snapshot,
134    /// Domain time, supplied; never read from a clock.
135    pub evaluation_time: Timestamp,
136    /// Can only tighten the plan's `deadline_ms`.
137    pub deadline_ms: Option<u64>,
138    /// Opaque complete authorized-scope handle, including any tenant and
139    /// principal isolation the host requires; passed through to backends and
140    /// included in every reusable-work key.
141    pub principal_handle: Vec<u8>,
142}
143
144#[derive(Debug, Clone, PartialEq)]
145pub enum Completion {
146    Judged(Box<Judgment>),
147    Cancelled,
148}
149
150#[derive(Debug)]
151pub enum Delivery {
152    Acknowledged { receipt: String },
153    Queued(DeliveryTicket),
154    Dropped { dropped_total: u64 },
155}
156
157#[derive(Debug)]
158pub struct Decided {
159    pub completion: Completion,
160    pub record: Arc<RunRecord>,
161    pub delivery: Delivery,
162}
163
164/// What [`Runtime::decide_with_capture`] captured (spec 004, 5.3).
165#[derive(Debug, Clone, PartialEq)]
166pub enum CaptureOutcome {
167    /// The decision was rejected: nothing ran, and there is nothing to
168    /// retain or replay.
169    NotAdmitted,
170    Captured(Capture),
171}
172
173/// The normal decide result, and beside it the capture, also when evidence
174/// delivery failed.
175#[derive(Debug)]
176pub struct CapturedDecision {
177    pub result: Result<Decided, DecideError>,
178    pub capture: CaptureOutcome,
179}
180
181/// Live counts, for operators and tests. In memory only.
182#[derive(Debug, Clone, PartialEq, Eq, Default)]
183pub struct Gauges {
184    pub in_flight: usize,
185    pub queued: usize,
186    pub rejected: u64,
187    pub abandoned: u64,
188    /// Attempts in flight per backend.
189    pub backend_in_flight: BTreeMap<String, usize>,
190    pub sink: SinkStats,
191}
192
193#[derive(Clone)]
194pub(crate) struct Registered {
195    pub backend: Arc<dyn DecisionBackend>,
196    pub permits: Arc<Semaphore>,
197    pub max: usize,
198    /// Who implements it (spec 033, 3.9): `upstream` unless registered as
199    /// the host's own.
200    pub origin: rustev_contract::actionability::Origin,
201}
202
203pub(crate) struct Inner {
204    pub clock: Arc<dyn Clock>,
205    pub config: RuntimeConfig,
206    pub backends: BTreeMap<String, Registered>,
207    pub sources: BTreeMap<String, acquisition::RegisteredSource>,
208    pub sink: Arc<dyn EvidenceSink>,
209    pub queue: Option<mpsc::Sender<Job>>,
210    pub stats: Arc<Stats>,
211    pub shared: Option<Arc<Ledger>>,
212    optimizer: optimization::Optimizer,
213    admission: Arc<Semaphore>,
214    queued: AtomicUsize,
215    rejected: AtomicU64,
216    abandoned: AtomicU64,
217    observer_panics: AtomicU64,
218    /// Decision ids submitted and not yet finished, so no two live
219    /// decisions share attempt ids (spec 003, 3.9.2).
220    live: Mutex<BTreeSet<String>>,
221}
222
223pub struct Runtime {
224    inner: Arc<Inner>,
225}
226
227pub struct RuntimeBuilder {
228    clock: Arc<dyn Clock>,
229    sink: Arc<dyn EvidenceSink>,
230    config: RuntimeConfig,
231    backends: Vec<(
232        Arc<dyn DecisionBackend>,
233        usize,
234        rustev_contract::actionability::Origin,
235    )>,
236    shared: Option<Arc<Ledger>>,
237    sources: Vec<(Arc<dyn rustev_core::seams::AcquisitionSource>, usize)>,
238}
239
240impl RuntimeBuilder {
241    /// Install a source by its spec 032 descriptor with a per-source attempt bound.
242    pub fn acquisition_source(
243        mut self,
244        source: Arc<dyn rustev_core::seams::AcquisitionSource>,
245        max_concurrency: usize,
246    ) -> Self {
247        self.sources.push((source, max_concurrency));
248        self
249    }
250
251    /// Register a backend with its own attempt concurrency limit.
252    pub fn backend(mut self, backend: Arc<dyn DecisionBackend>, max_concurrency: usize) -> Self {
253        self.backends.push((
254            backend,
255            max_concurrency,
256            rustev_contract::actionability::Origin::Upstream,
257        ));
258        self
259    }
260
261    /// Register a backend the host implements itself (spec 033, 3.9.1): its
262    /// failures are caused by the host, not an upstream service. A host
263    /// relaying a failure from its own upstream may still report it as
264    /// `upstream`; nothing reports the caller.
265    pub fn host_backend(
266        mut self,
267        backend: Arc<dyn DecisionBackend>,
268        max_concurrency: usize,
269    ) -> Self {
270        self.backends.push((
271            backend,
272            max_concurrency,
273            rustev_contract::actionability::Origin::Host,
274        ));
275        self
276    }
277
278    /// A ledger shared by every decision, reserved after each decision's own.
279    pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
280        self.shared = Some(ledger);
281        self
282    }
283
284    /// Build the runtime. Under a queued sink policy this spawns the delivery
285    /// worker, so it must run inside a Tokio runtime.
286    pub fn build(self) -> Result<Runtime, ConfigError> {
287        let c = self.config;
288        for (name, v) in [
289            ("max_in_flight", c.max_in_flight),
290            ("max_parallel_requests", c.max_parallel_requests),
291        ] {
292            if v == 0 {
293                return Err(ConfigError::Zero(name));
294            }
295        }
296        let mut backends = BTreeMap::new();
297        for (b, max, origin) in self.backends {
298            if max == 0 {
299                return Err(ConfigError::Zero("backend max_concurrency"));
300            }
301            let id = b.descriptor().backend_id.clone();
302            let r = Registered {
303                backend: b,
304                permits: Arc::new(Semaphore::new(max)),
305                max,
306                origin,
307            };
308            if backends.insert(id.clone(), r).is_some() {
309                return Err(ConfigError::DuplicateBackend(id));
310            }
311        }
312        let mut sources = BTreeMap::new();
313        for (source, max) in self.sources {
314            if max == 0 {
315                return Err(ConfigError::Zero("source max_concurrency"));
316            }
317            let descriptor = source.descriptor().clone();
318            if descriptor.id().is_err() {
319                return Err(ConfigError::InvalidSource(descriptor.name));
320            }
321            let name = descriptor.name.clone();
322            if sources
323                .insert(
324                    name.clone(),
325                    acquisition::RegisteredSource {
326                        source,
327                        descriptor,
328                        permits: Arc::new(Semaphore::new(max)),
329                    },
330                )
331                .is_some()
332            {
333                return Err(ConfigError::InvalidSource(name));
334            }
335        }
336        let stats = Arc::new(Stats::default());
337        let queue = match c.sink {
338            SinkPolicy::FailDecision { .. } => None,
339            SinkPolicy::Backpressure {
340                max_pending,
341                delivery_timeout_ms,
342                ..
343            }
344            | SinkPolicy::DropCounted {
345                max_pending,
346                delivery_timeout_ms,
347            } => {
348                if max_pending == 0 {
349                    return Err(ConfigError::Zero("max_pending"));
350                }
351                let (tx, rx) = mpsc::channel(max_pending);
352                tokio::spawn(sink::worker(
353                    rx,
354                    self.sink.clone(),
355                    self.clock.clone(),
356                    delivery_timeout_ms,
357                    stats.clone(),
358                ));
359                Some(tx)
360            }
361        };
362        Ok(Runtime {
363            inner: Arc::new(Inner {
364                clock: self.clock,
365                config: c,
366                backends,
367                sources,
368                sink: self.sink,
369                queue,
370                stats,
371                shared: self.shared,
372                optimizer: optimization::Optimizer::default(),
373                admission: Arc::new(Semaphore::new(c.max_in_flight)),
374                queued: AtomicUsize::new(0),
375                rejected: AtomicU64::new(0),
376                abandoned: AtomicU64::new(0),
377                observer_panics: AtomicU64::new(0),
378                live: Mutex::new(BTreeSet::new()),
379            }),
380        })
381    }
382}
383
384/// A backend a request can be served by, in fallback order.
385#[derive(Clone)]
386pub(crate) struct Target {
387    pub backend_id: String,
388    pub artifact: ArtifactId,
389    pub registered: Registered,
390}
391
392pub(crate) struct StepRuntime {
393    pub targets: Vec<Target>,
394    pub retry: RetryPolicy,
395    pub timeout: AttemptTimeout,
396    pub fallback_on: Vec<FailureClass>,
397}
398
399/// A compiled plan checked against this runtime's backends.
400pub struct PreparedPlan {
401    compiled: Arc<Compiled>,
402    pub(crate) steps: BTreeMap<String, StepRuntime>,
403    pub(crate) cost: CostPolicy,
404    execution: RecordedExecution,
405    pub(crate) optimization: Option<OptimizationPolicy>,
406}
407
408impl PreparedPlan {
409    pub fn compiled(&self) -> &Compiled {
410        &self.compiled
411    }
412
413    /// Identified cache namespace for host invalidation and audit.
414    pub fn optimization_namespace(&self) -> Option<String> {
415        self.optimization
416            .as_ref()
417            .map(|policy| optimization::namespace(&self.compiled.id, policy))
418    }
419}
420
421struct QueueSlot<'a>(&'a AtomicUsize);
422
423impl Drop for QueueSlot<'_> {
424    fn drop(&mut self) {
425        self.0.fetch_sub(1, Ordering::SeqCst);
426    }
427}
428
429struct Live<'a> {
430    set: &'a Mutex<BTreeSet<String>>,
431    id: String,
432}
433
434impl Drop for Live<'_> {
435    fn drop(&mut self) {
436        self.set
437            .lock()
438            .unwrap_or_else(|e| e.into_inner())
439            .remove(&self.id);
440    }
441}
442
443struct Abandon<'a> {
444    counter: &'a AtomicU64,
445    armed: bool,
446}
447
448impl Drop for Abandon<'_> {
449    fn drop(&mut self) {
450        if self.armed {
451            self.counter.fetch_add(1, Ordering::SeqCst);
452        }
453    }
454}
455
456impl Runtime {
457    pub fn builder(
458        clock: Arc<dyn Clock>,
459        sink: Arc<dyn EvidenceSink>,
460        config: RuntimeConfig,
461    ) -> RuntimeBuilder {
462        RuntimeBuilder {
463            clock,
464            sink,
465            config,
466            backends: vec![],
467            sources: vec![],
468            shared: None,
469        }
470    }
471
472    pub fn gauges(&self) -> Gauges {
473        let i = &self.inner;
474        Gauges {
475            in_flight: i.config.max_in_flight - i.admission.available_permits(),
476            queued: i.queued.load(Ordering::SeqCst),
477            rejected: i.rejected.load(Ordering::SeqCst),
478            abandoned: i.abandoned.load(Ordering::SeqCst),
479            backend_in_flight: i
480                .backends
481                .iter()
482                .map(|(k, r)| (k.clone(), r.max - r.permits.available_permits()))
483                .collect(),
484            sink: i.stats.snapshot(),
485        }
486    }
487
488    /// Check a compiled plan against the registered backends (spec 003,
489    /// 3.5.3 and 3.6.5).
490    pub fn prepare(&self, compiled: Compiled) -> Result<PreparedPlan, PrepareError> {
491        if compiled.plan.schema == schema::PLAN_V3 && !self.inner.sink.accepts(schema::RUN_V3) {
492            return Err(PrepareError::SinkVersionUnsupported {
493                schema: schema::RUN_V3,
494            });
495        }
496        let (policy, fallbacks, execution) = match &compiled.plan.execution {
497            PlanExecution::None => (None, vec![], RecordedExecution::None),
498            PlanExecution::Declared(d) => (
499                Some(d.policy.clone()),
500                d.fallbacks.clone(),
501                RecordedExecution::Declared(d.id.clone()),
502            ),
503        };
504        let cost = policy
505            .as_ref()
506            .map(|p| p.cost)
507            .unwrap_or(CostPolicy::Unlimited);
508        let optimization = policy.as_ref().and_then(|p| p.optimization.clone());
509        let target = |backend_id: &str, descriptor: &rustev_contract::ids::DescriptorId| {
510            let r = self
511                .inner
512                .backends
513                .get(backend_id)
514                .ok_or_else(|| PrepareError::MissingBackend(backend_id.to_string()))?;
515            if r.backend.descriptor().id().ok().as_ref() != Some(descriptor) {
516                return Err(PrepareError::DescriptorMismatch {
517                    backend_id: backend_id.to_string(),
518                });
519            }
520            let hard = matches!(cost, CostPolicy::Hard { .. })
521                || self
522                    .inner
523                    .shared
524                    .as_ref()
525                    .is_some_and(|l| l.mode() == CostMode::Hard);
526            let model = r.backend.cost_model();
527            if hard && model != CostModel::Bounded {
528                return Err(PrepareError::HardBudgetUnenforceable {
529                    backend_id: backend_id.to_string(),
530                    model,
531                });
532            }
533            Ok(r.clone())
534        };
535        // Fact-only sources describe snapshot evidence and need no runtime adapter.
536        for binding in compiled
537            .plan
538            .acquisitions
539            .iter()
540            .flatten()
541            .flat_map(|a| &a.sources)
542        {
543            let Some(source) = self.inner.sources.get(&binding.source.name) else {
544                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
545            };
546            if source.descriptor.id().ok().as_ref() != Some(&binding.source.id) {
547                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
548            }
549            let op = source
550                .descriptor
551                .operations
552                .iter()
553                .flatten()
554                .find(|o| o.name == binding.operation);
555            let hard = matches!(cost, CostPolicy::Hard { .. })
556                || self
557                    .inner
558                    .shared
559                    .as_ref()
560                    .is_some_and(|l| l.mode() == CostMode::Hard);
561            if op.is_none()
562                || (hard
563                    && !op.is_some_and(|o| {
564                        matches!(
565                            o.cost,
566                            rustev_contract::acquisition::OperationCost::Bounded { .. }
567                        )
568                    }))
569            {
570                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
571            }
572        }
573        let mut steps = BTreeMap::new();
574        for s in &compiled.plan.steps {
575            let PlanStepDetail::Semantic(b) = &s.detail else {
576                continue;
577            };
578            let mut targets = vec![Target {
579                backend_id: b.backend_id.clone(),
580                artifact: b.artifact.clone(),
581                registered: target(&b.backend_id, &b.descriptor)?,
582            }];
583            let mut fb: Vec<_> = fallbacks.iter().filter(|f| f.step == s.id).collect();
584            fb.sort_by_key(|f| f.target);
585            for f in fb {
586                targets.push(Target {
587                    backend_id: f.backend_id.clone(),
588                    artifact: f.artifact.clone(),
589                    registered: target(&f.backend_id, &f.descriptor)?,
590                });
591            }
592            let declared: Option<&StepExecution> = policy
593                .as_ref()
594                .and_then(|p| p.steps.iter().find(|x| x.step == s.id));
595            steps.insert(
596                s.id.clone(),
597                StepRuntime {
598                    targets,
599                    retry: declared
600                        .map(|d| d.retry.clone())
601                        .unwrap_or_else(RetryPolicy::none),
602                    timeout: declared
603                        .map(|d| d.attempt_timeout)
604                        .unwrap_or(AttemptTimeout::None),
605                    fallback_on: declared.map(|d| d.fallback.on.clone()).unwrap_or_default(),
606                },
607            );
608        }
609        Ok(PreparedPlan {
610            compiled: Arc::new(compiled),
611            steps,
612            cost,
613            execution,
614            optimization,
615        })
616    }
617
618    /// Invalidate every reusable result in this plan's namespace, for any
619    /// correction, revocation, erasure, artifact withdrawal or policy
620    /// change the host observed. The generation advances before entries are
621    /// removed, so a result captured earlier is never stored or served.
622    pub fn invalidate_plan(&self, plan: &PreparedPlan) -> Option<InvalidationResult> {
623        let namespace = plan.optimization_namespace()?;
624        Some(
625            self.inner
626                .optimizer
627                .invalidate(&InvalidationSelector::Namespace { namespace }),
628        )
629    }
630
631    /// Invalidate reusable results for one opaque authorized scope, as for
632    /// an erasure or a scope revocation.
633    pub fn invalidate_scope(
634        &self,
635        plan: &PreparedPlan,
636        principal_handle: &[u8],
637    ) -> Option<InvalidationResult> {
638        let namespace = plan.optimization_namespace()?;
639        Some(
640            self.inner
641                .optimizer
642                .invalidate(&InvalidationSelector::Scope {
643                    namespace,
644                    scope_id: optimization::scope_id(principal_handle),
645                }),
646        )
647    }
648
649    async fn admit(
650        &self,
651        deadline: Instant,
652        cancel: &CancelSignal,
653    ) -> Result<OwnedSemaphorePermit, Rejection> {
654        let i = &self.inner;
655        if let Ok(p) = i.admission.clone().try_acquire_owned() {
656            return Ok(p);
657        }
658        let mut n = i.queued.load(Ordering::SeqCst);
659        loop {
660            if n >= i.config.max_queued {
661                return Err(Rejection::Overloaded);
662            }
663            match i
664                .queued
665                .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
666            {
667                Ok(_) => break,
668                Err(now) => n = now,
669            }
670        }
671        let _slot = QueueSlot(&i.queued);
672        let mut acquire = Box::pin(i.admission.clone().acquire_owned());
673        let mut timer = i.clock.sleep_until(deadline);
674        match race(&mut acquire, cancel, &mut timer).await {
675            Raced::Done(Ok(p)) if i.clock.now() < deadline => Ok(p),
676            Raced::Done(Ok(_)) | Raced::Expired => Err(Rejection::QueueDeadline),
677            Raced::Done(Err(_)) => Err(Rejection::Overloaded),
678            Raced::Cancelled => Err(Rejection::Cancelled),
679        }
680    }
681
682    /// Run one decision (spec 003). `cancel` is the caller's cancellation.
683    pub async fn decide(
684        &self,
685        plan: &PreparedPlan,
686        req: DecisionRequest,
687        cancel: &CancelSignal,
688    ) -> Result<Decided, DecideError> {
689        self.run(plan, req, cancel, None, None, None).await
690    }
691
692    /// Run with a value-free observer. The ordinary entry point has no callbacks.
693    pub async fn decide_observed(
694        &self,
695        plan: &PreparedPlan,
696        req: DecisionRequest,
697        cancel: &CancelSignal,
698        observer: &dyn ProgressObserver,
699    ) -> Result<Decided, DecideError> {
700        self.run(plan, req, cancel, None, None, Some(observer))
701            .await
702    }
703
704    /// Contained progress callback panics across this runtime's decisions.
705    pub fn observer_panics(&self) -> u64 {
706        self.inner.observer_panics.load(Ordering::Relaxed)
707    }
708
709    /// [`Runtime::decide`] with opt-in bounded capture of every supplied
710    /// value (spec 004, 5.3). An invalid configuration is refused before
711    /// admission. Dropping the future returns nothing, as for `decide`.
712    pub async fn decide_with_capture(
713        &self,
714        plan: &PreparedPlan,
715        req: DecisionRequest,
716        cancel: &CancelSignal,
717        capture: &CaptureConfig,
718    ) -> Result<CapturedDecision, CaptureConfigError> {
719        capture.check()?;
720        let decision_id = req.decision_id.clone();
721        let mut capturing = capture::Capturing::new(capture);
722        let result = self
723            .run(plan, req, cancel, Some(&mut capturing), None, None)
724            .await;
725        let capture = match &result {
726            Err(DecideError::Rejected(_)) => CaptureOutcome::NotAdmitted,
727            Ok(Decided { record, .. }) | Err(DecideError::EvidenceNotDelivered { record, .. }) => {
728                CaptureOutcome::Captured(
729                    capturing.finish(&decision_id, record.termination == Termination::Cancelled),
730                )
731            }
732        };
733        Ok(CapturedDecision { result, capture })
734    }
735
736    async fn run(
737        &self,
738        plan: &PreparedPlan,
739        req: DecisionRequest,
740        cancel: &CancelSignal,
741        capture: Option<&mut capture::Capturing<'_>>,
742        live: Option<&live::LiveCtx<'_>>,
743        observer: Option<&dyn ProgressObserver>,
744    ) -> Result<Decided, DecideError> {
745        let i = &*self.inner;
746        // The runtime waits on its own child of the caller's signal, so a
747        // long-lived caller signal holds no waker of this decision after it
748        // ends (spec 003, 3.4.3).
749        let cancel = &cancel.child();
750        // Dropping this future at any point after here, queued or admitted,
751        // counts as abandoned (spec 003, 3.4.5).
752        let mut abandon = Abandon {
753            counter: &i.abandoned,
754            armed: true,
755        };
756        let mut reject = |r: Rejection| {
757            abandon.armed = false;
758            i.rejected.fetch_add(1, Ordering::SeqCst);
759            DecideError::Rejected(r)
760        };
761        let submitted = i.clock.now();
762        if req.decision_id.is_empty() || req.decision_id.len() > 256 {
763            return Err(reject(Rejection::InvalidRequest(
764                "a decision id is 1 to 256 bytes".into(),
765            )));
766        }
767        let compiled: &Compiled = &plan.compiled;
768        let ev = match Evaluation::start(compiled, &req.snapshot, req.evaluation_time) {
769            Ok(ev) => ev,
770            Err(e) => return Err(reject(Rejection::InvalidRequest(e.to_string()))),
771        };
772        if !i
773            .live
774            .lock()
775            .unwrap_or_else(|e| e.into_inner())
776            .insert(req.decision_id.clone())
777        {
778            return Err(reject(Rejection::InvalidRequest(
779                "a decision with this id is already running".into(),
780            )));
781        }
782        let _live = Live {
783            set: &i.live,
784            id: req.decision_id.clone(),
785        };
786        let deadline_ms = compiled
787            .plan
788            .definition
789            .limits
790            .deadline_ms
791            .min(req.deadline_ms.unwrap_or(u64::MAX));
792        let deadline = submitted.saturating_add(deadline_ms);
793        let _permit = match self.admit(deadline, cancel).await {
794            Ok(p) => p,
795            Err(r) => return Err(reject(r)),
796        };
797        let queued_ms = i.clock.now().saturating_sub(submitted);
798        let observing = observer.map(|observer| progress::Observing {
799            observer,
800            panics: &i.observer_panics,
801        });
802        if let Some(o) = &observing {
803            o.call(|o| o.admitted());
804        }
805
806        let ledger = Ledger::new(plan.cost);
807        let ev = std::sync::Mutex::new(ev);
808        let (requests, acquisitions, cancelled) = {
809            let cx = driver::DecisionCtx {
810                rt: i,
811                plan,
812                deadline,
813                submitted,
814                cancel,
815                ledger: &ledger,
816                decision_id: &req.decision_id,
817                principal: &req.principal_handle,
818                evaluation_time: req.evaluation_time,
819                ev: &ev,
820                live,
821                observing: observing.as_ref(),
822            };
823            driver::run_requests(&cx, capture).await
824        };
825        let ev = ev.into_inner().unwrap_or_else(|e| e.into_inner());
826        let (completion, core) = if cancelled {
827            (Completion::Cancelled, CoreEvidence::NotProduced)
828        } else {
829            // Every listed request was supplied unless cancelled; anything
830            // else is a defect, not a runtime condition.
831            let (judgment, evidence) = ev
832                .finish(&req.decision_id)
833                .expect("every pending request was supplied");
834            if let Some(o) = &observing {
835                o.remaining(&evidence, &requests, &compiled.plan);
836            }
837            (
838                Completion::Judged(Box::new(judgment)),
839                CoreEvidence::Recorded(Box::new(evidence)),
840            )
841        };
842        let elapsed_ms = i.clock.now().saturating_sub(submitted);
843        let plan_3 = plan.compiled.plan.schema == schema::PLAN_V3;
844        // A plan 2 decision's record is `rustev.run/2` exactly when the sink
845        // selected it (spec 020, 3.2).
846        let run_2 = !plan_3 && i.sink.run_schema() == schema::RUN_V2;
847        let (remote_evidence, remote_evidence_truncated) = if run_2 {
848            let (list, truncated) = remote_evidence(i, &requests);
849            (Some(list), truncated)
850        } else {
851            (None, false)
852        };
853        let mut record = RunRecord {
854            acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
855            // A plan 3 decision's record holds evidence 2 (spec 032, 3.10.3).
856            schema: if plan_3 {
857                schema::RUN_V3
858            } else if run_2 {
859                schema::RUN_V2
860            } else {
861                schema::RUN
862            }
863            .into(),
864            decision_id: req.decision_id.clone(),
865            plan_id: compiled.id.clone(),
866            execution: plan.execution.clone(),
867            termination: match completion {
868                Completion::Judged(_) => Termination::Judged,
869                Completion::Cancelled => Termination::Cancelled,
870            },
871            core,
872            timing: Timing {
873                queued_ms,
874                elapsed_ms,
875                deadline_ms,
876                deadline_expired: i.clock.now() >= deadline,
877            },
878            requests,
879            cost: ledger.summary(),
880            remote_evidence,
881            remote_evidence_truncated,
882        };
883        fit_remote_evidence(&mut record);
884        let record = Arc::new(record);
885        let delivery = self.deliver(record.clone()).await;
886        abandon.armed = false;
887        match delivery {
888            Ok(delivery) => Ok(Decided {
889                completion,
890                record,
891                delivery,
892            }),
893            Err(failure) => Err(DecideError::EvidenceNotDelivered { record, failure }),
894        }
895    }
896
897    async fn deliver(&self, record: Arc<RunRecord>) -> Result<Delivery, DeliveryFailure> {
898        let i = &*self.inner;
899        match i.config.sink {
900            SinkPolicy::FailDecision { timeout_ms } => {
901                sink::deliver_once(&*i.sink, &*i.clock, &record, timeout_ms, &i.stats)
902                    .await
903                    .map(|receipt| Delivery::Acknowledged { receipt })
904            }
905            SinkPolicy::Backpressure {
906                enqueue_timeout_ms, ..
907            } => {
908                let Some(tx) = &i.queue else {
909                    return Err(stopped());
910                };
911                let mut reserve = Box::pin(tx.reserve());
912                let mut timer = i
913                    .clock
914                    .sleep_until(i.clock.now().saturating_add(enqueue_timeout_ms));
915                match race(&mut reserve, &CancelSignal::new(), &mut timer).await {
916                    Raced::Done(Ok(permit)) => {
917                        let (done, rx) = oneshot::channel();
918                        i.stats.queued.fetch_add(1, Ordering::SeqCst);
919                        permit.send(Job { record, done });
920                        Ok(Delivery::Queued(DeliveryTicket(rx)))
921                    }
922                    Raced::Done(Err(_)) => Err(stopped()),
923                    Raced::Expired | Raced::Cancelled => {
924                        i.stats.failed.fetch_add(1, Ordering::SeqCst);
925                        Err(DeliveryFailure {
926                            detail: format!("no delivery slot within {enqueue_timeout_ms} ms"),
927                            uncertain: false,
928                        })
929                    }
930                }
931            }
932            SinkPolicy::DropCounted { .. } => {
933                let Some(tx) = &i.queue else {
934                    return Err(stopped());
935                };
936                let (done, rx) = oneshot::channel();
937                i.stats.queued.fetch_add(1, Ordering::SeqCst);
938                match tx.try_send(Job { record, done }) {
939                    Ok(()) => Ok(Delivery::Queued(DeliveryTicket(rx))),
940                    Err(mpsc::error::TrySendError::Closed(_)) => {
941                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
942                        Err(stopped())
943                    }
944                    Err(mpsc::error::TrySendError::Full(_)) => {
945                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
946                        let total = i.stats.dropped.fetch_add(1, Ordering::SeqCst) + 1;
947                        Ok(Delivery::Dropped {
948                            dropped_total: total,
949                        })
950                    }
951                }
952            }
953        }
954    }
955
956    /// A later observed charge for an attempt whose cost was unknown, applied
957    /// to the shared ledger (spec 003, 3.5.5). Delivered records are never
958    /// rewritten.
959    pub fn reconcile(&self, attempt_id: &str, observed_units: u64) -> bool {
960        self.inner
961            .shared
962            .as_ref()
963            .is_some_and(|l| l.reconcile(attempt_id, observed_units))
964    }
965}
966
967fn stopped() -> DeliveryFailure {
968    DeliveryFailure {
969        detail: "the delivery worker stopped".into(),
970        uncertain: false,
971    }
972}
973
974/// The remote evidence list of a `rustev.run/2` record (spec 020, 3.2): at
975/// most one entry per remote attempt, in attempt order, taken from the
976/// backend that ran it, with the step, instance and final remote state the
977/// record holds. Beyond [`MAX_REMOTE_EVIDENCE`] entries the list is cut and
978/// says so.
979fn remote_evidence(i: &Inner, requests: &[RequestRecord]) -> (Vec<RemoteEvidence>, bool) {
980    let mut out = Vec::new();
981    let mut truncated = false;
982    for r in requests {
983        for a in &r.attempts {
984            let Some(reg) = i.backends.get(&a.backend_id) else {
985                continue;
986            };
987            let Some(mut e) = reg.backend.take_remote_evidence(&a.attempt_id) else {
988                continue;
989            };
990            if out.len() >= MAX_REMOTE_EVIDENCE {
991                truncated = true;
992                continue;
993            }
994            e.attempt_id = a.attempt_id.clone();
995            e.step = r.step.clone();
996            e.instance = r.instance.clone();
997            e.remote_state = a.remote;
998            out.push(e.bounded());
999        }
1000    }
1001    (out, truncated)
1002}
1003
1004/// Keep a `rustev.run/2` record within its parse bounds: remote evidence
1005/// entries leave from the end, and the record says so, until it fits.
1006fn fit_remote_evidence(record: &mut RunRecord) {
1007    use rustev_contract::Document;
1008    let max = rustev_contract::limits::RECORD_V1.max_bytes;
1009    while record
1010        .remote_evidence
1011        .as_ref()
1012        .is_some_and(|l| !l.is_empty())
1013        && record.record_canonical().is_ok_and(|b| b.len() > max)
1014    {
1015        if let Some(l) = &mut record.remote_evidence {
1016            l.pop();
1017        }
1018        record.remote_evidence_truncated = true;
1019    }
1020}