1#![forbid(unsafe_code)]
14
15mod acquisition;
16mod capture;
17pub mod clock;
18mod driver;
19mod dynamic;
20mod generation;
21#[cfg(test)]
22mod generation_acceptance;
23pub use generation::{GenerationEvent, GenerationStream};
24pub mod ledger;
25mod live;
26mod optimization;
27mod progress;
28mod shared;
29mod store;
30pub use store::{
31 Invalidated, PutAnswer, PutTerms, RuntimeStore, StoreCall, StoreFailure, StoreSelector, Stored,
32};
33mod sink;
34pub use progress::{ProgressObserver, StepProgress};
35mod wait;
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
39use std::sync::{Arc, Mutex};
40
41use rustev_contract::execution::{
42 AttemptTimeout, CostPolicy, FailureClass, RetryPolicy, StepExecution,
43};
44use rustev_contract::ids::ArtifactId;
45use rustev_contract::judgment::Judgment;
46use rustev_contract::optimization::{DecisionCacheTier, OptimizationPolicy};
47use rustev_contract::plan::{PlanExecution, PlanStepDetail};
48use rustev_contract::replay::Capture;
49use rustev_contract::run::{
50 CoreEvidence, CostMode, CostModel, MAX_REMOTE_EVIDENCE, RecordedExecution, RemoteEvidence,
51 RequestRecord, RunRecord, Termination, Timing,
52};
53use rustev_contract::snapshot::Snapshot;
54use rustev_contract::time::Timestamp;
55use rustev_contract::{Identified, schema};
56use rustev_core::Compiled;
57use rustev_core::evaluate::Evaluation;
58use rustev_core::seams::{CancelSignal, DecisionBackend, EvidenceSink};
59use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
60
61pub use capture::{CaptureConfig, CaptureConfigError, MAX_CAPTURE_BYTES};
62pub use clock::{Clock, Instant, ManualClock, TokioClock};
63pub use dynamic::{DynamicAsked, DynamicBounds, DynamicDelivery, ask_dynamic};
65pub use ledger::Ledger;
66pub use live::{DispatchAnswer, LiveAuthorizer, LiveRefusal, LiveRequest, LiveResult};
67pub use optimization::{InvalidationResult, PersistentInvalidation};
68pub use sink::{DeliveryFailure, DeliveryTicket, SinkPolicy, SinkStats};
69
70use crate::optimization::InvalidationSelector;
71use crate::sink::{Job, Stats};
72use crate::wait::{Raced, race};
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct RuntimeConfig {
77 pub max_in_flight: usize,
79 pub max_queued: usize,
81 pub max_parallel_requests: usize,
83 pub sink: SinkPolicy,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ConfigError {
88 Zero(&'static str),
90 DuplicateBackend(String),
91 InvalidSource(String),
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum PrepareError {
97 SourceBinding(String),
98 MissingBackend(String),
100 DescriptorMismatch {
102 backend_id: String,
103 },
104 HardBudgetUnenforceable {
106 backend_id: String,
107 model: CostModel,
108 },
109 SinkVersionUnsupported {
112 schema: &'static str,
113 },
114 StoreMissing,
117 StoreContractMismatch,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum Rejection {
125 Overloaded,
126 QueueDeadline,
127 Cancelled,
128 InvalidRequest(String),
129}
130
131#[derive(Debug)]
132pub enum DecideError {
133 GenerationFailed(String),
135 Rejected(Rejection),
136 EvidenceNotDelivered {
139 record: Arc<RunRecord>,
140 failure: DeliveryFailure,
141 },
142}
143
144#[derive(Debug, Clone)]
146pub struct DecisionRequest {
147 pub decision_id: String,
149 pub snapshot: Snapshot,
150 pub evaluation_time: Timestamp,
152 pub deadline_ms: Option<u64>,
154 pub principal_handle: Vec<u8>,
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub enum Completion {
162 Judged(Box<Judgment>),
163 Cancelled,
164}
165
166#[derive(Debug)]
167pub enum Delivery {
168 Acknowledged { receipt: String },
169 Queued(DeliveryTicket),
170 Dropped { dropped_total: u64 },
171}
172
173#[derive(Debug)]
174pub struct Decided {
175 pub text_result: Option<rustev_contract::generation_result::TextResult>,
176 pub completion: Completion,
177 pub record: Arc<RunRecord>,
178 pub delivery: Delivery,
179 pub served_from_cache: Option<ServedFromCache>,
182 pub decision_cache_write: Option<Result<(), String>>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ServedFromCache {
195 pub entry_id: String,
196 pub source_run_record: rustev_contract::ids::ContentDigest,
198}
199
200#[derive(Debug, Clone, PartialEq)]
202pub enum CaptureOutcome {
203 NotAdmitted,
206 Captured(Capture),
207}
208
209#[derive(Debug)]
212pub struct CapturedDecision {
213 pub result: Result<Decided, DecideError>,
214 pub capture: CaptureOutcome,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Default)]
219pub struct Gauges {
220 pub in_flight: usize,
221 pub queued: usize,
222 pub rejected: u64,
223 pub abandoned: u64,
224 pub backend_in_flight: BTreeMap<String, usize>,
226 pub sink: SinkStats,
227}
228
229#[derive(Clone)]
230pub(crate) struct Registered {
231 pub backend: Arc<dyn DecisionBackend>,
232 pub permits: Arc<Semaphore>,
233 pub max: usize,
234 pub origin: rustev_contract::actionability::Origin,
237}
238
239pub(crate) struct Inner {
240 pub clock: Arc<dyn Clock>,
241 pub config: RuntimeConfig,
242 pub backends: BTreeMap<String, Registered>,
243 pub sources: BTreeMap<String, acquisition::RegisteredSource>,
244 pub sink: Arc<dyn EvidenceSink>,
245 pub queue: Option<mpsc::Sender<Job>>,
246 pub stats: Arc<Stats>,
247 pub shared: Option<Arc<Ledger>>,
248 pub store: Option<Arc<dyn RuntimeStore>>,
250 unavailable: Mutex<BTreeSet<(String, Option<String>)>>,
253 optimizer: optimization::Optimizer,
254 admission: Arc<Semaphore>,
255 queued: AtomicUsize,
256 rejected: AtomicU64,
257 abandoned: AtomicU64,
258 observer_panics: AtomicU64,
259 live: Mutex<BTreeSet<String>>,
262}
263
264impl Inner {
265 pub(crate) fn persistent_unavailable(&self, namespace: &str, scope_id: &str) -> bool {
268 let marks = self.unavailable.lock().unwrap_or_else(|e| e.into_inner());
269 marks.contains(&(namespace.to_string(), None))
270 || marks.contains(&(namespace.to_string(), Some(scope_id.to_string())))
271 }
272}
273
274pub struct Runtime {
275 inner: Arc<Inner>,
276}
277
278pub struct RuntimeBuilder {
279 clock: Arc<dyn Clock>,
280 sink: Arc<dyn EvidenceSink>,
281 config: RuntimeConfig,
282 backends: Vec<(
283 Arc<dyn DecisionBackend>,
284 usize,
285 rustev_contract::actionability::Origin,
286 )>,
287 shared: Option<Arc<Ledger>>,
288 sources: Vec<(Arc<dyn rustev_core::seams::AcquisitionSource>, usize)>,
289 store: Option<Arc<dyn RuntimeStore>>,
290}
291
292impl RuntimeBuilder {
293 pub fn acquisition_source(
295 mut self,
296 source: Arc<dyn rustev_core::seams::AcquisitionSource>,
297 max_concurrency: usize,
298 ) -> Self {
299 self.sources.push((source, max_concurrency));
300 self
301 }
302
303 pub fn backend(mut self, backend: Arc<dyn DecisionBackend>, max_concurrency: usize) -> Self {
305 self.backends.push((
306 backend,
307 max_concurrency,
308 rustev_contract::actionability::Origin::Upstream,
309 ));
310 self
311 }
312
313 pub fn host_backend(
318 mut self,
319 backend: Arc<dyn DecisionBackend>,
320 max_concurrency: usize,
321 ) -> Self {
322 self.backends.push((
323 backend,
324 max_concurrency,
325 rustev_contract::actionability::Origin::Host,
326 ));
327 self
328 }
329
330 pub fn store(mut self, store: Arc<dyn RuntimeStore>) -> Self {
333 self.store = Some(store);
334 self
335 }
336
337 pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
339 self.shared = Some(ledger);
340 self
341 }
342
343 pub fn build(self) -> Result<Runtime, ConfigError> {
346 let c = self.config;
347 for (name, v) in [
348 ("max_in_flight", c.max_in_flight),
349 ("max_parallel_requests", c.max_parallel_requests),
350 ] {
351 if v == 0 {
352 return Err(ConfigError::Zero(name));
353 }
354 }
355 let mut backends = BTreeMap::new();
356 for (b, max, origin) in self.backends {
357 if max == 0 {
358 return Err(ConfigError::Zero("backend max_concurrency"));
359 }
360 let id = b.descriptor().backend_id.clone();
361 let r = Registered {
362 backend: b,
363 permits: Arc::new(Semaphore::new(max)),
364 max,
365 origin,
366 };
367 if backends.insert(id.clone(), r).is_some() {
368 return Err(ConfigError::DuplicateBackend(id));
369 }
370 }
371 let mut sources = BTreeMap::new();
372 for (source, max) in self.sources {
373 if max == 0 {
374 return Err(ConfigError::Zero("source max_concurrency"));
375 }
376 let descriptor = source.descriptor().clone();
377 if descriptor.id().is_err() {
378 return Err(ConfigError::InvalidSource(descriptor.name));
379 }
380 let name = descriptor.name.clone();
381 if sources
382 .insert(
383 name.clone(),
384 acquisition::RegisteredSource {
385 source,
386 descriptor,
387 permits: Arc::new(Semaphore::new(max)),
388 },
389 )
390 .is_some()
391 {
392 return Err(ConfigError::InvalidSource(name));
393 }
394 }
395 let stats = Arc::new(Stats::default());
396 let queue = match c.sink {
397 SinkPolicy::FailDecision { .. } => None,
398 SinkPolicy::Backpressure {
399 max_pending,
400 delivery_timeout_ms,
401 ..
402 }
403 | SinkPolicy::DropCounted {
404 max_pending,
405 delivery_timeout_ms,
406 } => {
407 if max_pending == 0 {
408 return Err(ConfigError::Zero("max_pending"));
409 }
410 let (tx, rx) = mpsc::channel(max_pending);
411 tokio::spawn(sink::worker(
412 rx,
413 self.sink.clone(),
414 self.clock.clone(),
415 delivery_timeout_ms,
416 stats.clone(),
417 ));
418 Some(tx)
419 }
420 };
421 Ok(Runtime {
422 inner: Arc::new(Inner {
423 clock: self.clock,
424 config: c,
425 backends,
426 sources,
427 sink: self.sink,
428 queue,
429 stats,
430 shared: self.shared,
431 store: self.store,
432 unavailable: Mutex::new(BTreeSet::new()),
433 optimizer: optimization::Optimizer::default(),
434 admission: Arc::new(Semaphore::new(c.max_in_flight)),
435 queued: AtomicUsize::new(0),
436 rejected: AtomicU64::new(0),
437 abandoned: AtomicU64::new(0),
438 observer_panics: AtomicU64::new(0),
439 live: Mutex::new(BTreeSet::new()),
440 }),
441 })
442 }
443}
444
445#[derive(Clone)]
447pub(crate) struct Target {
448 pub backend_id: String,
449 pub artifact: ArtifactId,
450 pub registered: Registered,
451}
452
453pub(crate) struct StepRuntime {
454 pub targets: Vec<Target>,
455 pub extraction: Option<rustev_contract::extraction::ExtractionLimits>,
456 pub retry: RetryPolicy,
457 pub timeout: AttemptTimeout,
458 pub fallback_on: Vec<FailureClass>,
459}
460
461pub struct PreparedPlan {
463 compiled: Arc<Compiled>,
464 pub(crate) steps: BTreeMap<String, StepRuntime>,
465 pub(crate) cost: CostPolicy,
466 execution: RecordedExecution,
467 pub(crate) optimization: Option<OptimizationPolicy>,
468}
469
470impl PreparedPlan {
471 pub fn compiled(&self) -> &Compiled {
472 &self.compiled
473 }
474
475 pub fn optimization_namespace(&self) -> Option<String> {
477 self.optimization
478 .as_ref()
479 .map(|policy| optimization::namespace(&self.compiled.id, policy))
480 }
481}
482
483struct QueueSlot<'a>(&'a AtomicUsize);
484
485impl Drop for QueueSlot<'_> {
486 fn drop(&mut self) {
487 self.0.fetch_sub(1, Ordering::SeqCst);
488 }
489}
490
491struct Live<'a> {
492 set: &'a Mutex<BTreeSet<String>>,
493 id: String,
494}
495
496impl Drop for Live<'_> {
497 fn drop(&mut self) {
498 self.set
499 .lock()
500 .unwrap_or_else(|e| e.into_inner())
501 .remove(&self.id);
502 }
503}
504
505struct Abandon<'a> {
506 counter: &'a AtomicU64,
507 armed: bool,
508}
509
510impl Drop for Abandon<'_> {
511 fn drop(&mut self) {
512 if self.armed {
513 self.counter.fetch_add(1, Ordering::SeqCst);
514 }
515 }
516}
517
518impl Runtime {
519 pub fn builder(
520 clock: Arc<dyn Clock>,
521 sink: Arc<dyn EvidenceSink>,
522 config: RuntimeConfig,
523 ) -> RuntimeBuilder {
524 RuntimeBuilder {
525 clock,
526 sink,
527 config,
528 backends: vec![],
529 sources: vec![],
530 shared: None,
531 store: None,
532 }
533 }
534
535 pub fn gauges(&self) -> Gauges {
536 let i = &self.inner;
537 Gauges {
538 in_flight: i.config.max_in_flight - i.admission.available_permits(),
539 queued: i.queued.load(Ordering::SeqCst),
540 rejected: i.rejected.load(Ordering::SeqCst),
541 abandoned: i.abandoned.load(Ordering::SeqCst),
542 backend_in_flight: i
543 .backends
544 .iter()
545 .map(|(k, r)| (k.clone(), r.max - r.permits.available_permits()))
546 .collect(),
547 sink: i.stats.snapshot(),
548 }
549 }
550
551 pub fn backend_terms(
554 &self,
555 backend_id: &str,
556 ) -> Option<(&rustev_contract::descriptor::BackendDescriptor, CostModel)> {
557 let r = self.inner.backends.get(backend_id)?;
558 Some((r.backend.descriptor(), r.backend.cost_model()))
559 }
560
561 pub fn prepare(&self, compiled: Compiled) -> Result<PreparedPlan, PrepareError> {
564 if compiled.plan.schema == schema::PLAN_V3 && !self.inner.sink.accepts(schema::RUN_V3) {
565 return Err(PrepareError::SinkVersionUnsupported {
566 schema: schema::RUN_V3,
567 });
568 }
569 let (policy, fallbacks, execution) = match &compiled.plan.execution {
570 PlanExecution::None => (None, vec![], RecordedExecution::None),
571 PlanExecution::Declared(d) => (
572 Some(d.policy.clone()),
573 d.fallbacks.clone(),
574 RecordedExecution::Declared(d.id.clone()),
575 ),
576 };
577 let cost = policy
578 .as_ref()
579 .map(|p| p.cost)
580 .unwrap_or(CostPolicy::Unlimited);
581 let optimization = policy.as_ref().and_then(|p| p.optimization.clone());
582 if let Some(persistent) = optimization
583 .as_ref()
584 .and_then(|o| o.persistent_cache.as_ref())
585 {
586 let Some(store) = &self.inner.store else {
587 return Err(PrepareError::StoreMissing);
588 };
589 if *store.contract() != persistent.store_contract {
590 return Err(PrepareError::StoreContractMismatch);
591 }
592 }
593 let target = |backend_id: &str, descriptor: &rustev_contract::ids::DescriptorId| {
594 let r = self
595 .inner
596 .backends
597 .get(backend_id)
598 .ok_or_else(|| PrepareError::MissingBackend(backend_id.to_string()))?;
599 if r.backend.descriptor().id().ok().as_ref() != Some(descriptor) {
600 return Err(PrepareError::DescriptorMismatch {
601 backend_id: backend_id.to_string(),
602 });
603 }
604 let hard = matches!(cost, CostPolicy::Hard { .. })
605 || self
606 .inner
607 .shared
608 .as_ref()
609 .is_some_and(|l| l.mode() == CostMode::Hard);
610 let model = r.backend.cost_model();
611 if hard && model != CostModel::Bounded {
612 return Err(PrepareError::HardBudgetUnenforceable {
613 backend_id: backend_id.to_string(),
614 model,
615 });
616 }
617 Ok(r.clone())
618 };
619 for binding in compiled
621 .plan
622 .acquisitions
623 .iter()
624 .flatten()
625 .flat_map(|a| &a.sources)
626 {
627 let Some(source) = self.inner.sources.get(&binding.source.name) else {
628 return Err(PrepareError::SourceBinding(binding.source.name.clone()));
629 };
630 if source.descriptor.id().ok().as_ref() != Some(&binding.source.id) {
631 return Err(PrepareError::SourceBinding(binding.source.name.clone()));
632 }
633 let op = source
634 .descriptor
635 .operations
636 .iter()
637 .flatten()
638 .find(|o| o.name == binding.operation);
639 let hard = matches!(cost, CostPolicy::Hard { .. })
640 || self
641 .inner
642 .shared
643 .as_ref()
644 .is_some_and(|l| l.mode() == CostMode::Hard);
645 if op.is_none()
646 || (hard
647 && !op.is_some_and(|o| {
648 matches!(
649 o.cost,
650 rustev_contract::acquisition::OperationCost::Bounded { .. }
651 )
652 }))
653 {
654 return Err(PrepareError::SourceBinding(binding.source.name.clone()));
655 }
656 }
657 let mut steps = BTreeMap::new();
658 for s in &compiled.plan.steps {
659 let (backend_id, artifact, descriptor, extraction) = match &s.detail {
660 PlanStepDetail::Semantic(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
661 PlanStepDetail::Generate(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
662 PlanStepDetail::Extract(b) => (
663 &b.backend_id,
664 &b.artifact,
665 &b.descriptor,
666 Some(b.limits.clone()),
667 ),
668 PlanStepDetail::Rank(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
669 _ => continue,
670 };
671 let mut targets = vec![Target {
672 backend_id: backend_id.clone(),
673 artifact: artifact.clone(),
674 registered: target(backend_id, descriptor)?,
675 }];
676 let mut fb: Vec<_> = fallbacks.iter().filter(|f| f.step == s.id).collect();
677 fb.sort_by_key(|f| f.target);
678 for f in fb {
679 targets.push(Target {
680 backend_id: f.backend_id.clone(),
681 artifact: f.artifact.clone(),
682 registered: target(&f.backend_id, &f.descriptor)?,
683 });
684 }
685 let declared: Option<&StepExecution> = policy
686 .as_ref()
687 .and_then(|p| p.steps.iter().find(|x| x.step == s.id));
688 steps.insert(
689 s.id.clone(),
690 StepRuntime {
691 targets,
692 extraction,
693 retry: declared
694 .map(|d| d.retry.clone())
695 .unwrap_or_else(RetryPolicy::none),
696 timeout: declared
697 .map(|d| d.attempt_timeout)
698 .unwrap_or(AttemptTimeout::None),
699 fallback_on: declared.map(|d| d.fallback.on.clone()).unwrap_or_default(),
700 },
701 );
702 }
703 for s in &compiled.plan.steps {
704 if let PlanStepDetail::Generate(g) = &s.detail
705 && let Some(b) = &g.output_screener
706 {
707 for field in &g.output.fields {
708 if matches!(
709 field.ty,
710 rustev_contract::generation::GeneratedType::Text { .. }
711 ) {
712 steps.insert(
713 rustev_contract::generation::output_screen_step(&s.id, &field.name),
714 StepRuntime {
715 targets: vec![Target {
716 backend_id: b.backend_id.clone(),
717 artifact: b.artifact.clone(),
718 registered: target(&b.backend_id, &b.descriptor)?,
719 }],
720 extraction: None,
721 retry: RetryPolicy::none(),
722 timeout: AttemptTimeout::None,
723 fallback_on: vec![],
724 },
725 );
726 }
727 }
728 }
729 }
730 Ok(PreparedPlan {
731 compiled: Arc::new(compiled),
732 steps,
733 cost,
734 execution,
735 optimization,
736 })
737 }
738
739 pub fn invalidate_plan(&self, plan: &PreparedPlan) -> Option<InvalidationResult> {
747 let namespace = plan.optimization_namespace()?;
748 Some(self.invalidate_memory(plan, namespace, None))
749 }
750
751 pub fn invalidate_scope(
755 &self,
756 plan: &PreparedPlan,
757 principal_handle: &[u8],
758 ) -> Option<InvalidationResult> {
759 let namespace = plan.optimization_namespace()?;
760 Some(self.invalidate_memory(
761 plan,
762 namespace,
763 Some(optimization::scope_id(principal_handle)),
764 ))
765 }
766
767 pub async fn invalidate(
774 &self,
775 plan: &PreparedPlan,
776 principal_handle: Option<&[u8]>,
777 ) -> Option<InvalidationResult> {
778 let namespace = plan.optimization_namespace()?;
779 let scope = principal_handle.map(optimization::scope_id);
780 let mut result = self.invalidate_memory(plan, namespace.clone(), scope.clone());
781 let Some(store) = self
782 .inner
783 .store
784 .as_ref()
785 .filter(|_| result.persistent.is_some())
786 else {
787 return Some(result);
788 };
789 let selector = match &scope {
790 None => StoreSelector::Namespace,
791 Some(scope) => StoreSelector::Scope(scope.clone()),
792 };
793 let call = StoreCall {
794 remaining_ms: None,
795 cancel: CancelSignal::new(),
796 };
797 match store.invalidate(&namespace, selector, call).await {
798 Ok(done) => {
799 let mut marks = self.unavailable();
800 match &scope {
801 None => marks.retain(|(n, _)| *n != namespace),
803 Some(scope) => {
804 marks.remove(&(namespace.clone(), Some(scope.clone())));
805 }
806 }
807 result.persistent = Some(PersistentInvalidation::Done {
808 generation: done.generation,
809 removed: done.removed,
810 });
811 result.complete = !marks.contains(&(namespace.clone(), None));
814 }
815 Err(failure) => {
816 result.persistent = Some(PersistentInvalidation::Failed {
817 detail: failure.detail,
818 });
819 }
820 }
821 Some(result)
822 }
823
824 fn unavailable(&self) -> std::sync::MutexGuard<'_, BTreeSet<(String, Option<String>)>> {
825 self.inner
826 .unavailable
827 .lock()
828 .unwrap_or_else(|e| e.into_inner())
829 }
830
831 fn invalidate_memory(
834 &self,
835 plan: &PreparedPlan,
836 namespace: String,
837 scope: Option<String>,
838 ) -> InvalidationResult {
839 let selector = match &scope {
840 None => InvalidationSelector::Namespace {
841 namespace: namespace.clone(),
842 },
843 Some(scope_id) => InvalidationSelector::Scope {
844 namespace: namespace.clone(),
845 scope_id: scope_id.clone(),
846 },
847 };
848 let mut result = self.inner.optimizer.invalidate(&selector);
849 let persistent = plan
850 .optimization
851 .as_ref()
852 .is_some_and(|o| o.persistent_cache.is_some());
853 if persistent && self.inner.store.is_some() {
854 self.unavailable().insert((namespace, scope));
855 result.persistent = Some(PersistentInvalidation::Pending);
856 result.complete = false;
857 }
858 result
859 }
860
861 async fn admit(
862 &self,
863 deadline: Instant,
864 cancel: &CancelSignal,
865 ) -> Result<OwnedSemaphorePermit, Rejection> {
866 let i = &self.inner;
867 if let Ok(p) = i.admission.clone().try_acquire_owned() {
868 return Ok(p);
869 }
870 let mut n = i.queued.load(Ordering::SeqCst);
871 loop {
872 if n >= i.config.max_queued {
873 return Err(Rejection::Overloaded);
874 }
875 match i
876 .queued
877 .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
878 {
879 Ok(_) => break,
880 Err(now) => n = now,
881 }
882 }
883 let _slot = QueueSlot(&i.queued);
884 let mut acquire = Box::pin(i.admission.clone().acquire_owned());
885 let mut timer = i.clock.sleep_until(deadline);
886 match race(&mut acquire, cancel, &mut timer).await {
887 Raced::Done(Ok(p)) if i.clock.now() < deadline => Ok(p),
888 Raced::Done(Ok(_)) | Raced::Expired => Err(Rejection::QueueDeadline),
889 Raced::Done(Err(_)) => Err(Rejection::Overloaded),
890 Raced::Cancelled => Err(Rejection::Cancelled),
891 }
892 }
893
894 pub async fn decide(
896 &self,
897 plan: &PreparedPlan,
898 req: DecisionRequest,
899 cancel: &CancelSignal,
900 ) -> Result<Decided, DecideError> {
901 self.run(plan, req, cancel, None, None, None, None, &BTreeMap::new())
902 .await
903 }
904
905 pub async fn decide_keyed(
913 &self,
914 plan: &PreparedPlan,
915 req: DecisionRequest,
916 dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
917 cancel: &CancelSignal,
918 ) -> Result<Decided, DecideError> {
919 self.run(plan, req, cancel, None, None, None, None, dimensions)
920 .await
921 }
922
923 pub async fn decide_observed(
925 &self,
926 plan: &PreparedPlan,
927 req: DecisionRequest,
928 cancel: &CancelSignal,
929 observer: &dyn ProgressObserver,
930 ) -> Result<Decided, DecideError> {
931 self.run(
932 plan,
933 req,
934 cancel,
935 None,
936 None,
937 Some(observer),
938 None,
939 &BTreeMap::new(),
940 )
941 .await
942 }
943
944 pub fn observer_panics(&self) -> u64 {
946 self.inner.observer_panics.load(Ordering::Relaxed)
947 }
948
949 pub async fn decide_with_capture(
953 &self,
954 plan: &PreparedPlan,
955 req: DecisionRequest,
956 cancel: &CancelSignal,
957 capture: &CaptureConfig,
958 ) -> Result<CapturedDecision, CaptureConfigError> {
959 self.decide_capturing(plan, req, cancel, capture, None)
960 .await
961 }
962
963 async fn decide_capturing(
964 &self,
965 plan: &PreparedPlan,
966 req: DecisionRequest,
967 cancel: &CancelSignal,
968 capture: &CaptureConfig,
969 streaming: Option<&generation::Streaming>,
970 ) -> Result<CapturedDecision, CaptureConfigError> {
971 capture.check()?;
974 let decision_id = req.decision_id.clone();
975 let mut capturing = capture::Capturing::new(capture);
976 let result = self
977 .run(
978 plan,
979 req,
980 cancel,
981 Some(&mut capturing),
982 None,
983 None,
984 streaming,
985 &BTreeMap::new(),
986 )
987 .await;
988 let capture = match &result {
989 Err(DecideError::Rejected(_) | DecideError::GenerationFailed(_)) => {
990 CaptureOutcome::NotAdmitted
991 }
992 Ok(Decided { record, .. }) | Err(DecideError::EvidenceNotDelivered { record, .. }) => {
993 CaptureOutcome::Captured(
994 capturing.finish(&decision_id, record.termination == Termination::Cancelled),
995 )
996 }
997 };
998 Ok(CapturedDecision { result, capture })
999 }
1000
1001 pub fn decide_streaming<'a>(
1004 &'a self,
1005 plan: &'a PreparedPlan,
1006 req: DecisionRequest,
1007 cancel: &'a CancelSignal,
1008 stream_buffer: usize,
1009 ) -> Result<
1010 (
1011 impl std::future::Future<Output = Result<Decided, DecideError>> + Send + 'a,
1012 GenerationStream,
1013 ),
1014 rustev_core::generation_eval::GenerationError,
1015 > {
1016 let (streaming, receiver) = generation::Streaming::new(plan, stream_buffer)?;
1017 Ok((
1018 async move {
1019 self.run(
1020 plan,
1021 req,
1022 cancel,
1023 None,
1024 None,
1025 None,
1026 Some(&streaming),
1027 &BTreeMap::new(),
1028 )
1029 .await
1030 },
1031 receiver,
1032 ))
1033 }
1034
1035 #[allow(clippy::too_many_arguments)]
1036 async fn run(
1037 &self,
1038 plan: &PreparedPlan,
1039 req: DecisionRequest,
1040 cancel: &CancelSignal,
1041 mut capture: Option<&mut capture::Capturing<'_>>,
1042 live: Option<&live::LiveCtx<'_>>,
1043 observer: Option<&dyn ProgressObserver>,
1044 streaming: Option<&generation::Streaming>,
1045 dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
1046 ) -> Result<Decided, DecideError> {
1047 let i = &*self.inner;
1048 let cancel = &cancel.child();
1052 let mut abandon = Abandon {
1055 counter: &i.abandoned,
1056 armed: true,
1057 };
1058 let mut reject = |r: Rejection| {
1059 abandon.armed = false;
1060 i.rejected.fetch_add(1, Ordering::SeqCst);
1061 DecideError::Rejected(r)
1062 };
1063 let submitted = i.clock.now();
1064 if req.decision_id.is_empty() || req.decision_id.len() > 256 {
1065 return Err(reject(Rejection::InvalidRequest(
1066 "a decision id is 1 to 256 bytes".into(),
1067 )));
1068 }
1069 let compiled: &Compiled = &plan.compiled;
1070 let ev = match Evaluation::start(compiled, &req.snapshot, req.evaluation_time) {
1071 Ok(ev) => ev,
1072 Err(e) => return Err(reject(Rejection::InvalidRequest(e.to_string()))),
1073 };
1074 if !i
1075 .live
1076 .lock()
1077 .unwrap_or_else(|e| e.into_inner())
1078 .insert(req.decision_id.clone())
1079 {
1080 return Err(reject(Rejection::InvalidRequest(
1081 "a decision with this id is already running".into(),
1082 )));
1083 }
1084 let _live = Live {
1085 set: &i.live,
1086 id: req.decision_id.clone(),
1087 };
1088 let deadline_ms = compiled
1089 .plan
1090 .definition
1091 .limits
1092 .deadline_ms
1093 .min(req.deadline_ms.unwrap_or(u64::MAX));
1094 let deadline = submitted.saturating_add(deadline_ms);
1095 let _permit = match self.admit(deadline, cancel).await {
1096 Ok(p) => p,
1097 Err(r) => return Err(reject(r)),
1098 };
1099 let queued_ms = i.clock.now().saturating_sub(submitted);
1100 let observing = observer.map(|observer| progress::Observing {
1101 observer,
1102 panics: &i.observer_panics,
1103 });
1104 if let Some(o) = &observing {
1105 o.call(|o| o.admitted());
1106 }
1107
1108 let ledger = Ledger::new(plan.cost);
1109 let ev = std::sync::Mutex::new(ev);
1110 let deferred_cache = Mutex::new(vec![]);
1111 let persistent = plan
1114 .optimization
1115 .as_ref()
1116 .and_then(|o| o.persistent_cache.as_ref())
1117 .filter(|_| i.store.is_some() && live.is_none());
1118 let mut pending = store::Pending::default();
1119 let mut decision = optimization::decision::DecisionRun::start(
1122 &i.optimizer,
1123 plan,
1124 &req,
1125 dimensions,
1126 live.is_some(),
1127 );
1128 let decision_policy = plan
1129 .optimization
1130 .as_ref()
1131 .and_then(|o| o.decision_cache.as_ref());
1132 let decision_store = match (persistent, i.store.as_deref()) {
1134 (Some(persistent), Some(store)) => Some(store::StoreCtx {
1135 rt: i,
1136 store,
1137 persistent,
1138 cancel,
1139 deadline,
1140 }),
1141 _ => None,
1142 };
1143 if let (Some(d), Some(calls), Some(policy), Some(cache)) = (
1144 decision.as_mut(),
1145 decision_store.as_ref(),
1146 plan.optimization.as_ref(),
1147 decision_policy,
1148 ) {
1149 d.read_store(&i.optimizer, policy, cache, calls, req.evaluation_time)
1150 .await;
1151 }
1152 let has_generation = compiled
1153 .plan
1154 .steps
1155 .iter()
1156 .any(|s| matches!(s.detail, PlanStepDetail::Generate(_)));
1157 let (mut requests, acquisitions, mut cancelled) = {
1158 let cx = driver::DecisionCtx {
1159 rt: i,
1160 plan,
1161 deadline,
1162 submitted,
1163 cancel,
1164 ledger: &ledger,
1165 decision_id: &req.decision_id,
1166 principal: &req.principal_handle,
1167 evaluation_time: req.evaluation_time,
1168 ev: Some(&ev),
1169 generation: None,
1170 step_ledger: None,
1171 stream: None,
1172 item_clock: None,
1173 deferred_cache: has_generation.then_some(&deferred_cache),
1174 persistent: persistent.map(|_| &pending),
1175 decision: decision.as_ref(),
1176 live,
1177 observing: observing.as_ref(),
1178 };
1179 driver::run_requests(&cx, capture.as_deref_mut()).await
1180 };
1181 let ev = ev.into_inner().unwrap_or_else(|e| e.into_inner());
1182 let unresolved_volatile: Vec<String> = decision_policy
1185 .filter(|_| decision.as_ref().is_some_and(|d| d.hit.is_some()))
1186 .map(|p| {
1187 p.volatile
1188 .iter()
1189 .filter(|v| !v.starts_with("step:"))
1190 .filter(|v| ev.status_of(&format!("input:{v}")).is_err())
1191 .cloned()
1192 .collect()
1193 })
1194 .unwrap_or_default();
1195 let stage_cx = generation::StageContext {
1196 rt: i,
1197 plan,
1198 deadline,
1199 submitted,
1200 cancel,
1201 ledger: &ledger,
1202 decision_id: &req.decision_id,
1203 principal: &req.principal_handle,
1204 evaluation_time: req.evaluation_time,
1205 streaming,
1206 };
1207 let mut text_result = None;
1208 let mut writes = vec![];
1209 let mut judged = false;
1210 let (completion, core) = if cancelled {
1214 (Completion::Cancelled, CoreEvidence::NotProduced)
1215 } else {
1216 let stage = ev
1217 .begin_generation(&req.decision_id)
1218 .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1219 let stage = Mutex::new(stage);
1220 let (generation_requests, pending_writes, was_cancelled) =
1227 generation::run(&stage_cx, &stage, capture)
1228 .await
1229 .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1230 requests.extend(generation_requests);
1231 cancelled = was_cancelled;
1232 if cancelled {
1233 (Completion::Cancelled, CoreEvidence::NotProduced)
1234 } else {
1235 let (judgment, evidence) = {
1236 let stage = stage.lock().unwrap_or_else(|e| e.into_inner());
1237 let (mut judgment, evidence, mut text) = stage
1238 .finish()
1239 .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1240 if let Some(text) = &mut text {
1241 stage.verify_ownership(&mut judgment, text);
1242 if let Some(streaming) = streaming {
1243 streaming.finish(text);
1244 }
1245 }
1246 if let Some(o) = &observing {
1247 o.remaining(&evidence, &requests, &compiled.plan);
1248 }
1249 text_result = text;
1250 (judgment, evidence)
1251 };
1252 if let (Some(persistent), Some(store), Some(policy)) =
1256 (persistent, i.store.as_deref(), plan.optimization.as_ref())
1257 {
1258 let written = store::write_pending(
1259 std::mem::take(&mut pending),
1260 store::Writing {
1261 rt: i,
1262 store,
1263 policy,
1264 persistent,
1265 cancel,
1266 deadline,
1267 created: req.evaluation_time,
1268 evidence: rustev_contract::Document::record_digest(&evidence).ok(),
1269 },
1270 )
1271 .await;
1272 for ((step, instance), diagnostic) in written {
1273 if let Some(o) = requests
1274 .iter_mut()
1275 .find(|r| r.step == step && r.instance == instance)
1276 .and_then(|r| r.optimization.as_mut())
1277 {
1278 o.diagnose(diagnostic);
1279 }
1280 }
1281 }
1282 writes = deferred_cache
1283 .into_inner()
1284 .unwrap_or_else(|e| e.into_inner());
1285 writes.extend(pending_writes);
1286 judged = true;
1287 (
1288 Completion::Judged(Box::new(judgment)),
1289 CoreEvidence::Recorded(Box::new(evidence)),
1290 )
1291 }
1292 };
1293 let elapsed_ms = i.clock.now().saturating_sub(submitted);
1294 let plan_3 = plan.compiled.plan.schema == schema::PLAN_V3;
1295 let run_2 = !plan_3 && i.sink.run_schema() == schema::RUN_V2;
1298 let (remote_evidence, remote_evidence_truncated) = if run_2 {
1299 let (list, truncated) = remote_evidence(i, &requests);
1300 (Some(list), truncated)
1301 } else {
1302 (None, false)
1303 };
1304 let admission = match (&decision, decision_policy) {
1307 (Some(d), Some(policy)) if judged => match d.admissible(&requests, policy) {
1308 Ok(admit) => admit,
1309 Err(diagnostic) => {
1310 d.diagnose(diagnostic);
1311 None
1312 }
1313 },
1314 _ => None,
1315 };
1316 let served_from_cache =
1317 decision
1318 .as_ref()
1319 .and_then(|d| d.hit.as_ref())
1320 .map(|hit| ServedFromCache {
1321 entry_id: hit.entry_id.clone(),
1322 source_run_record: hit.entry.source_run_record.clone(),
1323 });
1324 let mut record = RunRecord {
1325 acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
1326 decision_cache: decision
1327 .as_ref()
1328 .map(|d| d.record(&requests, &unresolved_volatile)),
1329 schema: if plan_3 {
1331 schema::RUN_V3
1332 } else if run_2 {
1333 schema::RUN_V2
1334 } else {
1335 schema::RUN
1336 }
1337 .into(),
1338 decision_id: req.decision_id.clone(),
1339 plan_id: compiled.id.clone(),
1340 execution: plan.execution.clone(),
1341 termination: match completion {
1342 Completion::Judged(_) => Termination::Judged,
1343 Completion::Cancelled => Termination::Cancelled,
1344 },
1345 core,
1346 timing: Timing {
1347 queued_ms,
1348 elapsed_ms,
1349 deadline_ms,
1350 deadline_expired: i.clock.now() >= deadline,
1351 },
1352 requests,
1353 cost: ledger.summary(),
1354 remote_evidence,
1355 remote_evidence_truncated,
1356 };
1357 fit_remote_evidence(&mut record);
1358 let record = Arc::new(record);
1359 let delivery = self.deliver(record.clone()).await;
1360 abandon.armed = false;
1361 match delivery {
1362 Ok(delivery) => {
1363 generation::write_cache(&stage_cx, writes);
1364 let mut decision_cache_write = None;
1365 if let (Some(d), Some(policy), Some((key, requests)), Ok(source)) = (
1366 &decision,
1367 decision_policy,
1368 admission,
1369 rustev_contract::Document::record_digest(&*record),
1370 ) {
1371 if let (Some(calls), Some(generations)) = (&decision_store, d.captured)
1372 && policy.tiers.contains(&DecisionCacheTier::Persistent)
1373 {
1374 use rustev_contract::Document;
1375 let digests = match (&completion, &record.core) {
1376 (Completion::Judged(j), CoreEvidence::Recorded(e)) => {
1377 j.record_digest().ok().zip(e.record_digest().ok())
1378 }
1379 _ => None,
1380 };
1381 let volatile = req
1382 .snapshot
1383 .entries
1384 .iter()
1385 .filter(|e| policy.volatile.contains(&e.field))
1386 .cloned()
1387 .collect();
1388 decision_cache_write = Some(match (digests, plan.optimization.as_ref()) {
1389 (Some((judgment, evidence)), Some(optimization)) => {
1390 store::write_decision(
1391 calls,
1392 store::DecisionWrite {
1393 key: &key,
1394 policy: optimization,
1395 requests: &requests,
1396 volatile,
1397 judgment,
1398 run_record: source.clone(),
1399 evidence,
1400 generations,
1401 created: req.evaluation_time,
1402 ttl_ms: policy.ttl_ms,
1403 },
1404 )
1405 .await
1406 }
1407 _ => {
1408 Err("persistent decision cache entry has no canonical form".into())
1409 }
1410 });
1411 }
1412 if policy.tiers.contains(&DecisionCacheTier::Memory) {
1416 let _ = i.optimizer.insert_decision(
1417 policy,
1418 key,
1419 optimization::decision::DecisionEntry {
1420 requests,
1421 source_run_record: source,
1422 created: req.evaluation_time,
1423 },
1424 d.generation,
1425 );
1426 }
1427 }
1428 Ok(Decided {
1429 text_result,
1430 completion,
1431 record,
1432 delivery,
1433 served_from_cache,
1434 decision_cache_write,
1435 })
1436 }
1437 Err(failure) => Err(DecideError::EvidenceNotDelivered { record, failure }),
1438 }
1439 }
1440
1441 async fn deliver(&self, record: Arc<RunRecord>) -> Result<Delivery, DeliveryFailure> {
1442 let i = &*self.inner;
1443 match i.config.sink {
1444 SinkPolicy::FailDecision { timeout_ms } => {
1445 sink::deliver_once(&*i.sink, &*i.clock, &record, timeout_ms, &i.stats)
1446 .await
1447 .map(|receipt| Delivery::Acknowledged { receipt })
1448 }
1449 SinkPolicy::Backpressure {
1450 enqueue_timeout_ms, ..
1451 } => {
1452 let Some(tx) = &i.queue else {
1453 return Err(stopped());
1454 };
1455 let mut reserve = Box::pin(tx.reserve());
1456 let mut timer = i
1457 .clock
1458 .sleep_until(i.clock.now().saturating_add(enqueue_timeout_ms));
1459 match race(&mut reserve, &CancelSignal::new(), &mut timer).await {
1460 Raced::Done(Ok(permit)) => {
1461 let (done, rx) = oneshot::channel();
1462 i.stats.queued.fetch_add(1, Ordering::SeqCst);
1463 permit.send(Job { record, done });
1464 Ok(Delivery::Queued(DeliveryTicket(rx)))
1465 }
1466 Raced::Done(Err(_)) => Err(stopped()),
1467 Raced::Expired | Raced::Cancelled => {
1468 i.stats.failed.fetch_add(1, Ordering::SeqCst);
1469 Err(DeliveryFailure {
1470 detail: format!("no delivery slot within {enqueue_timeout_ms} ms"),
1471 uncertain: false,
1472 })
1473 }
1474 }
1475 }
1476 SinkPolicy::DropCounted { .. } => {
1477 let Some(tx) = &i.queue else {
1478 return Err(stopped());
1479 };
1480 let (done, rx) = oneshot::channel();
1481 i.stats.queued.fetch_add(1, Ordering::SeqCst);
1482 match tx.try_send(Job { record, done }) {
1483 Ok(()) => Ok(Delivery::Queued(DeliveryTicket(rx))),
1484 Err(mpsc::error::TrySendError::Closed(_)) => {
1485 i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1486 Err(stopped())
1487 }
1488 Err(mpsc::error::TrySendError::Full(_)) => {
1489 i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1490 let total = i.stats.dropped.fetch_add(1, Ordering::SeqCst) + 1;
1491 Ok(Delivery::Dropped {
1492 dropped_total: total,
1493 })
1494 }
1495 }
1496 }
1497 }
1498 }
1499
1500 pub fn reconcile(&self, attempt_id: &str, observed_units: u64) -> bool {
1504 self.inner
1505 .shared
1506 .as_ref()
1507 .is_some_and(|l| l.reconcile(attempt_id, observed_units))
1508 }
1509}
1510
1511fn stopped() -> DeliveryFailure {
1512 DeliveryFailure {
1513 detail: "the delivery worker stopped".into(),
1514 uncertain: false,
1515 }
1516}
1517
1518fn remote_evidence(i: &Inner, requests: &[RequestRecord]) -> (Vec<RemoteEvidence>, bool) {
1524 let mut out = Vec::new();
1525 let mut truncated = false;
1526 for r in requests {
1527 for a in &r.attempts {
1528 let Some(reg) = i.backends.get(&a.backend_id) else {
1529 continue;
1530 };
1531 let Some(mut e) = reg.backend.take_remote_evidence(&a.attempt_id) else {
1532 continue;
1533 };
1534 if out.len() >= MAX_REMOTE_EVIDENCE {
1535 truncated = true;
1536 continue;
1537 }
1538 e.attempt_id = a.attempt_id.clone();
1539 e.step = r.step.clone();
1540 e.instance = r.instance.clone();
1541 e.remote_state = a.remote;
1542 out.push(e.bounded());
1543 }
1544 }
1545 (out, truncated)
1546}
1547
1548fn fit_remote_evidence(record: &mut RunRecord) {
1551 use rustev_contract::Document;
1552 let max = rustev_contract::limits::RECORD_V1.max_bytes;
1553 while record
1554 .remote_evidence
1555 .as_ref()
1556 .is_some_and(|l| !l.is_empty())
1557 && record.record_canonical().is_ok_and(|b| b.len() > max)
1558 {
1559 if let Some(l) = &mut record.remote_evidence {
1560 l.pop();
1561 }
1562 record.remote_evidence_truncated = true;
1563 }
1564}
1565
1566mod spend;
1567pub use spend::{
1568 LiveSpend, SpendAttempt, SpendBinding, SpendGate, SpendRefusal, SpendTicket, SpendUnit,
1569};