1use std::panic::{AssertUnwindSafe, catch_unwind};
15
16use rustev_contract::dynamic::{
17 AbstentionDecision, DynamicAttempt, DynamicCapability, DynamicOutcome, DynamicRaw,
18 DynamicRefusal, DynamicResult, DynamicTiming, DynamicUnresolved, DynamicValidation,
19};
20use rustev_contract::execution::{CostPolicy, FailureClass};
21use rustev_contract::ids::DynamicRequestId;
22use rustev_contract::remote::ServedIdentity;
23use rustev_contract::run::{
24 AttemptCost, AttemptEnd, CancelAnswer, Cancellation, Charge, CostBound, RemoteState,
25};
26use rustev_contract::time::DurationMs;
27use rustev_contract::{Identified, schema};
28use rustev_core::dynamic::{self as core, ValidatedQuestion};
29use rustev_core::seams::{
30 AdapterFailure, CallContext, CancelAck, CancelSignal, DynamicBackend, DynamicCall,
31 DynamicEvidenceSink, DynamicReport, RemoteEnd,
32};
33
34use crate::clock::{Clock, Instant};
35use crate::driver::{DETAIL_BYTES, answered, escape};
36use crate::ledger::Ledger;
37use crate::sink::DeliveryFailure;
38use crate::wait::{CatchUnwind, Raced, cut, poll_once, race};
39
40#[derive(Clone, Copy)]
42pub struct DynamicBounds<'a> {
43 pub clock: &'a dyn Clock,
44 pub deadline_ms: Option<u64>,
46 pub shared_ledger: Option<&'a Ledger>,
49 pub sink: Option<(&'a dyn DynamicEvidenceSink, u64)>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum DynamicDelivery {
57 NotRequested,
58 Acknowledged { receipt: String },
59 Failed(DeliveryFailure),
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct DynamicAsked {
65 pub outcome: DynamicOutcome,
66 pub delivery: DynamicDelivery,
67}
68
69struct Observed {
71 served: ServedIdentity,
72 attempt: Option<DynamicAttempt>,
73 raw: DynamicRaw,
74 validation: DynamicValidation,
75 abstention: AbstentionDecision,
76 result: DynamicResult,
77}
78
79struct Building<'q> {
80 q: &'q ValidatedQuestion,
81 backend_id: String,
82 start: Instant,
83 deadline: Instant,
84 deadline_ms: u64,
85 ledger: Ledger,
86}
87
88impl Building<'_> {
89 fn outcome(
90 &self,
91 clock: &dyn Clock,
92 capability: Option<&DynamicCapability>,
93 request: Option<DynamicRequestId>,
94 o: Observed,
95 ) -> DynamicOutcome {
96 let q = self.q.question();
97 let now = clock.now();
98 DynamicOutcome {
99 schema: schema::DYNAMIC_OUTCOME.into(),
100 request_id: q.request_id.clone(),
101 request,
102 scope: q.scope.clone(),
103 task_family: q.task_family.clone(),
104 operation: q.operation,
105 output: q.output,
106 backend_id: self.backend_id.clone(),
107 requested_artifact: q.artifact.clone(),
108 served: o.served,
109 capability: capability.and_then(|c| c.id().ok()),
110 template: capability.map(|c| c.template.clone()),
111 privacy: q.privacy.clone(),
112 attempt: o.attempt,
113 timing: DynamicTiming {
114 elapsed_ms: now.saturating_sub(self.start),
115 deadline_ms: self.deadline_ms,
116 deadline_expired: now >= self.deadline,
117 },
118 cost: self.ledger.summary(),
119 raw: o.raw,
120 validation: o.validation,
121 abstention: o.abstention,
122 result: o.result,
123 }
124 }
125
126 fn undispatched(
128 &self,
129 clock: &dyn Clock,
130 capability: Option<&DynamicCapability>,
131 request: Option<DynamicRequestId>,
132 u: DynamicUnresolved,
133 ) -> DynamicOutcome {
134 self.outcome(
135 clock,
136 capability,
137 request,
138 Observed {
139 served: ServedIdentity::unknown(),
140 attempt: None,
141 raw: DynamicRaw::NotDispatched,
142 validation: DynamicValidation::NotRun,
143 abstention: AbstentionDecision::NotApplied,
144 result: DynamicResult::Unresolved(u),
145 },
146 )
147 }
148}
149
150fn refused(r: DynamicRefusal) -> DynamicUnresolved {
151 DynamicUnresolved::Refused { refusal: r }
152}
153
154enum Ended {
156 Report(DynamicReport),
157 Failed(FailureClass, String),
158 Deadline,
159 Cancelled,
160}
161
162pub async fn ask_dynamic(
167 question: &ValidatedQuestion,
168 backend: &dyn DynamicBackend,
169 bounds: DynamicBounds<'_>,
170 cx: CallContext,
171 cancel: &CancelSignal,
172) -> DynamicAsked {
173 let outcome = run(question, backend, &bounds, cx, cancel).await;
174 let delivery = match bounds.sink {
175 None => DynamicDelivery::NotRequested,
176 Some((sink, timeout_ms)) => deliver(sink, bounds.clock, &outcome, timeout_ms).await,
177 };
178 DynamicAsked { outcome, delivery }
179}
180
181async fn run(
182 question: &ValidatedQuestion,
183 backend: &dyn DynamicBackend,
184 bounds: &DynamicBounds<'_>,
185 cx: CallContext,
186 cancel: &CancelSignal,
187) -> DynamicOutcome {
188 let clock = bounds.clock;
189 let q = question.question();
190 let start = clock.now();
191 let deadline_ms = q.deadline_ms.min(bounds.deadline_ms.unwrap_or(u64::MAX));
192 let b = Building {
193 q: question,
194 backend_id: backend.backend_id().to_string(),
195 start,
196 deadline: start.saturating_add(deadline_ms),
197 deadline_ms,
198 ledger: Ledger::new(CostPolicy::Hard {
200 max_units: q.max_cost_units,
201 }),
202 };
203 let cancel = &cancel.child();
204 let early = |b: &Building<'_>| {
205 if cancel.is_raised() {
206 Some(DynamicUnresolved::Cancelled)
207 } else if clock.now() >= b.deadline {
208 Some(DynamicUnresolved::DeadlineExceeded)
209 } else {
210 None
211 }
212 };
213 if let Some(u) = early(&b) {
214 return b.undispatched(clock, None, None, u);
215 }
216 let capability = match core::admit(question, backend.dynamic_capability()) {
218 Ok(c) => c,
219 Err(r) => {
220 let c = backend.dynamic_capability();
221 let request = c.and_then(|c| core::request_identity(question, c).ok());
222 return b.undispatched(clock, c, request, refused(r));
223 }
224 };
225 let request = match core::request_identity(question, capability) {
226 Ok(id) => id,
227 Err(e) => {
228 return b.undispatched(
229 clock,
230 Some(capability),
231 None,
232 refused(DynamicRefusal::CapabilityInvalid {
233 detail: cut(&e.to_string(), DETAIL_BYTES),
234 }),
235 );
236 }
237 };
238 let projection = core::projection(question);
239 let bound = match catch_unwind(AssertUnwindSafe(|| backend.cost_bound(&projection))) {
241 Ok(bound) => bound,
242 Err(_) => CostBound::Unknown,
243 };
244 let units = match core::admit_cost(bound, q.max_cost_units) {
245 Ok(u) => u,
246 Err(r) => return b.undispatched(clock, Some(capability), Some(request), refused(r)),
247 };
248 if b.ledger.reserve(units).is_err() {
249 return b.undispatched(
250 clock,
251 Some(capability),
252 Some(request),
253 refused(DynamicRefusal::CostAboveCap {
254 bound: units,
255 cap: q.max_cost_units,
256 }),
257 );
258 }
259 if let Some(shared) = bounds.shared_ledger
260 && shared.reserve(units).is_err()
261 {
262 b.ledger.release(units);
263 return b.undispatched(
264 clock,
265 Some(capability),
266 Some(request),
267 refused(DynamicRefusal::BudgetExhausted),
268 );
269 }
270 let release = |b: &Building<'_>| {
271 b.ledger.release(units);
272 if let Some(shared) = bounds.shared_ledger {
273 shared.release(units);
274 }
275 };
276 if let Some(u) = early(&b) {
277 release(&b);
278 return b.undispatched(clock, Some(capability), Some(request), u);
279 }
280
281 let attempt_id = format!("{}/1", escape(&q.request_id));
283 let now = clock.now();
284 let dispatched_ms = now.saturating_sub(start);
285 let call_cx = CallContext {
286 remaining_ms: DurationMs(b.deadline.saturating_sub(now)),
287 ..cx
288 };
289 let signal = cancel.child();
290 let call = DynamicCall {
291 projection: &projection,
292 template: &capability.template,
293 attempt_id: &attempt_id,
294 cancel: signal.clone(),
295 cx: &call_cx,
296 };
297 let (ended, cancellation, remote, charge) =
298 match catch_unwind(AssertUnwindSafe(|| backend.ask(call))) {
299 Err(p) => (
300 Ended::Failed(
301 FailureClass::AdapterFault,
302 format!(
303 "adapter panicked: {}",
304 crate::wait::panic_detail(p.as_ref())
305 ),
306 ),
307 Cancellation::NotRequested,
308 RemoteState::PossiblyContinuing,
309 Charge::Unknown,
310 ),
311 Ok(work) => {
312 let mut work = CatchUnwind::new(work);
313 let mut timer = clock.sleep_until(b.deadline);
314 match race(&mut work, cancel, &mut timer).await {
315 Raced::Done(Ok(report))
316 if signal.is_raised()
317 && matches!(report.result, Err((AdapterFailure::Cancelled, _))) =>
318 {
319 let (c, r, ch) = answered(report.cancel, report.charge);
320 (Ended::Cancelled, c, r, ch)
321 }
322 Raced::Done(Ok(report)) => {
323 let cancellation = if signal.is_raised() {
324 Cancellation::Requested {
325 answer: match report.cancel {
326 CancelAck::Stopped => CancelAnswer::Stopped,
327 _ => CancelAnswer::Unconfirmed,
328 },
329 }
330 } else {
331 Cancellation::NotRequested
332 };
333 let (remote, charge) = match report.remote {
334 RemoteEnd::PossiblyContinuing if !signal.is_raised() => {
335 (RemoteState::PossiblyContinuing, Charge::Unknown)
336 }
337 _ => (RemoteState::Finished, report.charge),
338 };
339 (Ended::Report(report), cancellation, remote, charge)
340 }
341 Raced::Done(Err(panic)) => (
342 Ended::Failed(
343 FailureClass::AdapterFault,
344 format!("adapter panicked: {panic}"),
345 ),
346 Cancellation::NotRequested,
347 RemoteState::PossiblyContinuing,
348 Charge::Unknown,
349 ),
350 raced => {
351 let why = match raced {
352 Raced::Cancelled => Ended::Cancelled,
353 _ => Ended::Deadline,
354 };
355 signal.raise();
359 let (c, r, ch) = match poll_once(&mut work).await {
360 Some(Ok(report)) => answered(report.cancel, report.charge),
361 Some(Err(_)) | None => (
362 Cancellation::Requested {
363 answer: CancelAnswer::NotObserved,
364 },
365 RemoteState::PossiblyContinuing,
366 Charge::Unknown,
367 ),
368 };
369 (why, c, r, ch)
370 }
371 }
372 }
373 };
374 let ended_ms = clock.now().saturating_sub(start);
375 b.ledger.settle(&attempt_id, units, charge, true);
376 if let Some(shared) = bounds.shared_ledger {
377 shared.settle(&attempt_id, units, charge, true);
378 }
379 let attempt = |end: AttemptEnd| DynamicAttempt {
380 attempt_id: attempt_id.clone(),
381 dispatched_ms,
382 ended_ms,
383 end,
384 cancellation,
385 remote,
386 cost: AttemptCost {
387 bound,
388 reserved: units,
389 charge,
390 },
391 };
392 let unresolved_attempt =
393 |end: AttemptEnd, served: ServedIdentity, raw: DynamicRaw, u: DynamicUnresolved| {
394 b.outcome(
395 clock,
396 Some(capability),
397 Some(request.clone()),
398 Observed {
399 served,
400 attempt: Some(attempt(end)),
401 raw,
402 validation: DynamicValidation::NotRun,
403 abstention: AbstentionDecision::NotApplied,
404 result: DynamicResult::Unresolved(u),
405 },
406 )
407 };
408 match ended {
409 Ended::Cancelled => unresolved_attempt(
410 AttemptEnd::Cancelled,
411 ServedIdentity::unknown(),
412 DynamicRaw::NoAnswer,
413 DynamicUnresolved::Cancelled,
414 ),
415 Ended::Deadline => unresolved_attempt(
416 AttemptEnd::Deadline,
417 ServedIdentity::unknown(),
418 DynamicRaw::NoAnswer,
419 DynamicUnresolved::DeadlineExceeded,
420 ),
421 Ended::Failed(class, detail) => {
422 let detail = cut(&detail, DETAIL_BYTES);
423 unresolved_attempt(
424 AttemptEnd::Failed {
425 class,
426 detail: detail.clone(),
427 },
428 ServedIdentity::unknown(),
429 DynamicRaw::Failed {
430 class,
431 detail: detail.clone(),
432 },
433 DynamicUnresolved::BackendFailed { class, detail },
434 )
435 }
436 Ended::Report(report) => {
437 let served = bounded_served(report.served);
438 match report.result {
439 Err((failure, detail)) => {
440 let class = match failure {
441 AdapterFailure::Transient => FailureClass::Transient,
442 AdapterFailure::Overloaded => FailureClass::Overloaded,
443 AdapterFailure::Permanent => FailureClass::Permanent,
444 AdapterFailure::Cancelled => FailureClass::AdapterFault,
445 };
446 let detail = cut(&detail, DETAIL_BYTES);
447 unresolved_attempt(
448 AttemptEnd::Failed {
449 class,
450 detail: detail.clone(),
451 },
452 served,
453 DynamicRaw::Failed {
454 class,
455 detail: detail.clone(),
456 },
457 DynamicUnresolved::BackendFailed { class, detail },
458 )
459 }
460 Ok(raw) => {
461 let raw = bounded_raw(raw);
462 let judged = core::judge(question, capability, &served, &raw);
463 let end = match &judged.validation {
464 DynamicValidation::Invalid { detail } => AttemptEnd::Failed {
465 class: FailureClass::InvalidOutput,
466 detail: cut(detail, DETAIL_BYTES),
467 },
468 _ => AttemptEnd::Output,
469 };
470 let result = match judged.result {
471 Ok(answer) => DynamicResult::Proposal(core::proposal(
472 question,
473 request.clone(),
474 capability,
475 &attempt_id,
476 answer,
477 )),
478 Err(u) => DynamicResult::Unresolved(u),
479 };
480 b.outcome(
481 clock,
482 Some(capability),
483 Some(request.clone()),
484 Observed {
485 served,
486 attempt: Some(attempt(end)),
487 raw: DynamicRaw::Output(raw),
488 validation: judged.validation,
489 abstention: judged.abstention,
490 result,
491 },
492 )
493 }
494 }
495 }
496 }
497}
498
499fn bounded_served(mut s: ServedIdentity) -> ServedIdentity {
501 s.identity = s.identity.map(|i| cut(&i, DETAIL_BYTES));
502 s
503}
504
505fn bounded_raw(
512 raw: rustev_contract::dynamic::DynamicRawOutput,
513) -> rustev_contract::dynamic::DynamicRawOutput {
514 use rustev_contract::dynamic::{DynamicRawOutput as R, ResponseShape};
515 match raw {
516 R::Label(l) => R::Label(cut(&l, DETAIL_BYTES)),
517 R::Distribution(m) if m.values().any(|v| !v.is_finite()) => {
518 let text = m
519 .iter()
520 .take(rustev_contract::dynamic::MAX_CLASSIFY_LABELS + 1)
521 .map(|(k, v)| format!("{:?}={v}", k.chars().take(64).collect::<String>()))
522 .collect::<Vec<_>>()
523 .join(", ");
524 R::Unrecognized {
525 shape: ResponseShape::NonFiniteMass,
526 excerpt: cut(&text, DETAIL_BYTES),
527 }
528 }
529 R::Distribution(m) => R::Distribution(
530 m.into_iter()
531 .take(rustev_contract::dynamic::MAX_CLASSIFY_LABELS + 1)
532 .map(|(k, v)| (cut(&k, DETAIL_BYTES), v))
533 .collect(),
534 ),
535 R::Unrecognized { shape, excerpt } => R::Unrecognized {
536 shape,
537 excerpt: cut(&excerpt, DETAIL_BYTES),
538 },
539 }
540}
541
542async fn deliver(
543 sink: &dyn DynamicEvidenceSink,
544 clock: &dyn Clock,
545 outcome: &DynamicOutcome,
546 timeout_ms: u64,
547) -> DynamicDelivery {
548 let work = match catch_unwind(AssertUnwindSafe(|| sink.deliver(outcome))) {
549 Ok(f) => f,
550 Err(p) => {
551 return DynamicDelivery::Failed(DeliveryFailure {
552 detail: cut(
553 &format!(
554 "the sink panicked: {}",
555 crate::wait::panic_detail(p.as_ref())
556 ),
557 DETAIL_BYTES,
558 ),
559 uncertain: true,
560 });
561 }
562 };
563 let mut work = CatchUnwind::new(work);
564 let mut timer = clock.sleep_until(clock.now().saturating_add(timeout_ms));
565 match race(&mut work, &CancelSignal::new(), &mut timer).await {
566 Raced::Done(Ok(Ok(receipt))) => DynamicDelivery::Acknowledged { receipt },
567 Raced::Done(Ok(Err(e))) => DynamicDelivery::Failed(DeliveryFailure {
568 detail: cut(&e, DETAIL_BYTES),
569 uncertain: false,
570 }),
571 Raced::Done(Err(p)) => DynamicDelivery::Failed(DeliveryFailure {
572 detail: cut(&format!("the sink panicked: {p}"), DETAIL_BYTES),
573 uncertain: true,
574 }),
575 Raced::Expired | Raced::Cancelled => DynamicDelivery::Failed(DeliveryFailure {
576 detail: format!("no acknowledgement within {timeout_ms} ms"),
577 uncertain: true,
578 }),
579 }
580}