1#![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};
54pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct RuntimeConfig {
68 pub max_in_flight: usize,
70 pub max_queued: usize,
72 pub max_parallel_requests: usize,
74 pub sink: SinkPolicy,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ConfigError {
79 Zero(&'static str),
81 DuplicateBackend(String),
82 InvalidSource(String),
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum PrepareError {
88 SourceBinding(String),
89 MissingBackend(String),
91 DescriptorMismatch {
93 backend_id: String,
94 },
95 HardBudgetUnenforceable {
97 backend_id: String,
98 model: CostModel,
99 },
100 SinkVersionUnsupported {
103 schema: &'static str,
104 },
105}
106
107#[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 EvidenceNotDelivered {
123 record: Arc<RunRecord>,
124 failure: DeliveryFailure,
125 },
126}
127
128#[derive(Debug, Clone)]
130pub struct DecisionRequest {
131 pub decision_id: String,
133 pub snapshot: Snapshot,
134 pub evaluation_time: Timestamp,
136 pub deadline_ms: Option<u64>,
138 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#[derive(Debug, Clone, PartialEq)]
166pub enum CaptureOutcome {
167 NotAdmitted,
170 Captured(Capture),
171}
172
173#[derive(Debug)]
176pub struct CapturedDecision {
177 pub result: Result<Decided, DecideError>,
178 pub capture: CaptureOutcome,
179}
180
181#[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 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 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 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 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 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 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 pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
280 self.shared = Some(ledger);
281 self
282 }
283
284 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#[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
399pub 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 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 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 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 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 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 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 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 pub fn observer_panics(&self) -> u64 {
706 self.inner.observer_panics.load(Ordering::Relaxed)
707 }
708
709 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 let cancel = &cancel.child();
750 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 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 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 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 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
974fn 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
1004fn 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}