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