1use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::convert::Infallible;
11use std::io;
12use std::net::SocketAddr;
13use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::Duration;
16
17use http_body_util::{BodyExt, Full, Limited};
18use hyper::body::{Bytes, Incoming};
19use hyper::header::{AUTHORIZATION, CONTENT_TYPE};
20use hyper::service::service_fn;
21use hyper::{Method, Request, Response, StatusCode};
22use hyper_util::rt::TokioIo;
23use rustev_contract::canonical::canonical_value_bytes;
24use rustev_contract::descriptor::BackendDescriptor;
25use rustev_contract::ids::DescriptorId;
26use rustev_contract::remote::{
27 Batching, CancelItem, CancelRequest, CancelResponse, CancelSupport, DescribeRequest,
28 DescribeResponse, Disclosure, HostedBackend, InferRequest, InferResponse, ItemAnswer,
29 ItemResult, MAX_ITEMS, MAX_TEXT_BYTES, PROTOCOL_MAJOR, RemoteCancelAnswer, RemoteCode,
30 RemoteTerms, ServedIdentity, ServedTerms, cut_to,
31};
32use rustev_contract::run::{Charge, CostBound, CostModel};
33use rustev_contract::time::DurationMs;
34use rustev_contract::{Document, Identified, schema};
35use rustev_core::seams::{
36 AdapterFailure, AttemptCall, AttemptReport, CallContext, CancelAck, CancelSignal,
37 DecisionBackend, RemoteEnd,
38};
39use tokio::net::TcpListener;
40use tokio::sync::watch;
41use tokio::task::JoinHandle;
42use tokio::time::Instant;
43
44use crate::cost::sum_charges;
45use crate::http::Secret;
46use crate::taxonomy::{code_of_detail, failure_class, is_local_only};
47use crate::wire::digest_bytes;
48
49pub const DESCRIBE_PATH: &str = "/rustev/remote/1/describe";
51pub const INFER_PATH: &str = "/rustev/remote/1/infer";
52pub const CANCEL_PATH: &str = "/rustev/remote/1/cancel";
53
54#[derive(Clone)]
56pub struct ServerConfig {
57 pub bearer: Option<Secret>,
59 pub idempotency_window_ms: u64,
60 pub max_request_bytes: u64,
61 pub max_response_bytes: u64,
62 pub cancellation: CancelSupport,
63 pub batching: Batching,
65 pub refusals_charged: bool,
67 pub max_in_flight: usize,
69 pub cancel_wait_ms: u64,
71 pub max_remembered: usize,
74 pub versions: Vec<u32>,
78}
79
80impl Default for ServerConfig {
81 fn default() -> Self {
82 ServerConfig {
83 bearer: None,
84 idempotency_window_ms: 10 * 60 * 1000,
85 max_request_bytes: 1024 * 1024,
86 max_response_bytes: 1024 * 1024,
87 cancellation: CancelSupport::Confirmed,
88 batching: Batching::None,
89 refusals_charged: false,
90 max_in_flight: 64,
91 cancel_wait_ms: 1_000,
92 max_remembered: 10_000,
93 versions: vec![PROTOCOL_MAJOR],
94 }
95 }
96}
97
98#[derive(Clone)]
100pub struct Hosted {
101 pub backend: Arc<dyn DecisionBackend>,
102 pub served: ServedTerms,
105 pub cost: Option<CostBound>,
107 pub privacy: Vec<String>,
108}
109
110impl Hosted {
111 pub fn new(backend: Arc<dyn DecisionBackend>) -> Self {
112 let identity = backend.descriptor().artifact.as_str().to_string();
113 Hosted {
114 backend,
115 served: ServedTerms {
116 disclosure: Disclosure::Pinned,
117 identity: Some(identity),
118 },
119 cost: None,
120 privacy: vec![],
121 }
122 }
123
124 fn cost(&self) -> CostBound {
125 if let Some(c) = self.cost {
126 return c;
127 }
128 let bound = self.backend.cost_bound(b"{}");
129 match (self.backend.cost_model(), bound) {
130 (CostModel::Bounded, b @ CostBound::Bounded { .. }) => b,
131 (CostModel::Unknown, _) => CostBound::Unknown,
132 (_, CostBound::Bounded { max_units }) => CostBound::Estimated { units: max_units },
133 (_, b) => b,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
140struct Recorded {
141 result: ItemResult,
142 charge: Charge,
143 ack: CancelAck,
144 remote: RemoteEnd,
145}
146
147struct Entry {
148 backend_id: String,
149 content: String,
150 signal: CancelSignal,
151 cancel_requested: Arc<AtomicBool>,
152 done: watch::Receiver<Option<Recorded>>,
153 finished_at: Option<Instant>,
154}
155
156struct State {
157 config: ServerConfig,
158 hosted: Mutex<BTreeMap<String, Hosted>>,
159 attempts: Mutex<HashMap<String, Entry>>,
160 in_flight: AtomicUsize,
161 dispatched: AtomicU64,
162}
163
164#[derive(Clone)]
166pub struct RemoteServer {
167 state: Arc<State>,
168}
169
170pub struct ServerHandle {
172 pub addr: SocketAddr,
173 task: JoinHandle<()>,
174}
175
176impl Drop for ServerHandle {
177 fn drop(&mut self) {
178 self.task.abort();
179 }
180}
181
182impl ServerHandle {
183 pub fn url(&self) -> String {
185 format!("http://{}", self.addr)
186 }
187}
188
189fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
190 m.lock().unwrap_or_else(|e| e.into_inner())
191}
192
193impl RemoteServer {
194 pub fn new(config: ServerConfig) -> Self {
195 RemoteServer {
196 state: Arc::new(State {
197 config,
198 hosted: Mutex::new(BTreeMap::new()),
199 attempts: Mutex::new(HashMap::new()),
200 in_flight: AtomicUsize::new(0),
201 dispatched: AtomicU64::new(0),
202 }),
203 }
204 }
205
206 pub fn host(&self, hosted: Hosted) {
210 let id = hosted.backend.descriptor().backend_id.clone();
211 lock(&self.state.hosted).insert(id, hosted);
212 }
213
214 pub fn dispatched(&self) -> u64 {
216 self.state.dispatched.load(Ordering::SeqCst)
217 }
218
219 pub fn serve(
222 &self,
223 listener: TcpListener,
224 tls: Option<Arc<rustls::ServerConfig>>,
225 ) -> io::Result<ServerHandle> {
226 let addr = listener.local_addr()?;
227 if tls.is_none() && !addr.ip().is_loopback() {
228 return Err(io::Error::new(
229 io::ErrorKind::InvalidInput,
230 "TLS is required except for loopback",
231 ));
232 }
233 let acceptor = tls.map(tokio_rustls::TlsAcceptor::from);
234 let state = self.state.clone();
235 let task = tokio::spawn(async move {
236 loop {
237 let Ok((stream, _)) = listener.accept().await else {
238 continue;
239 };
240 let _ = stream.set_nodelay(true);
241 let state = state.clone();
242 let acceptor = acceptor.clone();
243 tokio::spawn(async move {
244 match acceptor {
245 None => serve_conn(state, stream).await,
246 Some(a) => {
247 if let Ok(tls) = a.accept(stream).await {
248 serve_conn(state, tls).await
249 }
250 }
251 }
252 });
253 }
254 });
255 Ok(ServerHandle { addr, task })
256 }
257
258 pub async fn serve_loopback(&self) -> io::Result<ServerHandle> {
261 let listener = TcpListener::bind(("127.0.0.1", 0)).await?;
262 self.serve(listener, None)
263 }
264}
265
266async fn serve_conn<S>(state: Arc<State>, stream: S)
267where
268 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
269{
270 let svc = service_fn(move |req| {
271 let state = state.clone();
272 async move { Ok::<_, Infallible>(handle(state, req).await) }
273 });
274 let _ = hyper::server::conn::http1::Builder::new()
275 .serve_connection(TokioIo::new(stream), svc)
276 .await;
277}
278
279fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
282 if a.len() != b.len() {
283 return false;
284 }
285 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
286}
287
288fn reply(status: StatusCode, body: Vec<u8>) -> Response<Full<Bytes>> {
289 let mut r = Response::new(Full::new(Bytes::from(body)));
290 *r.status_mut() = status;
291 r.headers_mut()
292 .insert(CONTENT_TYPE, "application/json".parse().expect("static"));
293 r
294}
295
296fn refuse(status: StatusCode, why: &str) -> Response<Full<Bytes>> {
297 let body = serde_json::json!({ "error": cut_to(why, MAX_TEXT_BYTES) });
298 reply(status, serde_json::to_vec(&body).unwrap_or_default())
299}
300
301struct InFlight<'a>(&'a AtomicUsize);
302
303impl Drop for InFlight<'_> {
304 fn drop(&mut self) {
305 self.0.fetch_sub(1, Ordering::SeqCst);
306 }
307}
308
309async fn handle(state: Arc<State>, req: Request<Incoming>) -> Response<Full<Bytes>> {
310 if let Some(b) = &state.config.bearer {
311 let expected = format!("Bearer {}", b.expose());
312 let ok = req
313 .headers()
314 .get(AUTHORIZATION)
315 .is_some_and(|v| constant_time_eq(v.as_bytes(), expected.as_bytes()));
316 if !ok {
317 return refuse(StatusCode::UNAUTHORIZED, "credentials refused");
318 }
319 }
320 if req.method() != Method::POST {
321 return refuse(StatusCode::METHOD_NOT_ALLOWED, "POST only");
322 }
323 let n = state.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
324 let _guard = InFlight(&state.in_flight);
325 if n > state.config.max_in_flight {
326 return refuse(StatusCode::SERVICE_UNAVAILABLE, "overloaded");
327 }
328 let path = req.uri().path().to_string();
329 let max = state.config.max_request_bytes as usize;
330 let body = match Limited::new(req.into_body(), max).collect().await {
331 Ok(b) => b.to_bytes(),
332 Err(_) => return refuse(StatusCode::PAYLOAD_TOO_LARGE, "request too large"),
333 };
334 let r = match path.as_str() {
335 DESCRIBE_PATH => describe(&state, &body),
336 INFER_PATH => infer(&state, &body).await,
337 CANCEL_PATH => cancel(&state, &body).await,
338 _ => return refuse(StatusCode::NOT_FOUND, "no such document"),
339 };
340 match r {
341 Ok(bytes) if bytes.len() as u64 <= state.config.max_response_bytes => {
342 reply(StatusCode::OK, bytes)
343 }
344 Ok(_) => refuse(StatusCode::INTERNAL_SERVER_ERROR, "response too large"),
345 Err((status, why)) => refuse(status, &why),
346 }
347}
348
349type Refusal = (StatusCode, String);
350
351fn bad(why: impl Into<String>) -> Refusal {
352 (StatusCode::BAD_REQUEST, why.into())
353}
354
355fn terms(state: &State, h: &Hosted) -> RemoteTerms {
356 RemoteTerms {
357 cost: h.cost(),
358 refusals_charged: state.config.refusals_charged,
359 cancellation: state.config.cancellation,
360 served: h.served.clone(),
361 batching: state.config.batching,
362 max_request_bytes: state.config.max_request_bytes,
363 max_response_bytes: state.config.max_response_bytes,
364 idempotency_window_ms: state.config.idempotency_window_ms,
365 privacy: h.privacy.clone(),
366 }
367}
368
369fn describe(state: &State, body: &[u8]) -> Result<Vec<u8>, Refusal> {
370 let req = DescribeRequest::parse(body).map_err(|e| bad(e.to_string()))?;
371 let Some(version) = req
372 .versions
373 .iter()
374 .filter(|v| state.config.versions.contains(v))
375 .max()
376 .copied()
377 else {
378 return Err(bad("no common protocol version"));
379 };
380 let hosted = lock(&state.hosted).clone();
381 let mut backends = vec![];
382 for h in hosted.values() {
383 let descriptor = h.backend.descriptor().clone();
384 let descriptor_id = descriptor
385 .id()
386 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
387 backends.push(HostedBackend {
388 descriptor,
389 descriptor_id,
390 terms: terms(state, h),
391 });
392 }
393 DescribeResponse {
394 schema: schema::REMOTE_DESCRIBE.into(),
395 version,
396 backends,
397 }
398 .record_canonical()
399 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
400}
401
402fn failure(code: RemoteCode, detail: &str) -> ItemResult {
403 ItemResult::Failure {
404 code,
405 detail: cut_to(detail, MAX_TEXT_BYTES),
406 }
407}
408
409fn refused_all(req: &InferRequest, code: RemoteCode, detail: &str) -> Vec<(String, Recorded)> {
410 req.items
411 .iter()
412 .map(|i| {
413 (
414 i.attempt_id.clone(),
415 Recorded {
416 result: failure(code, detail),
417 charge: Charge::Observed { units: 0 },
418 ack: CancelAck::NotRequested,
419 remote: RemoteEnd::Finished,
420 },
421 )
422 })
423 .collect()
424}
425
426fn state_bytes(projection: &str) -> Option<Vec<u8>> {
428 let v: serde_json::Value = serde_json::from_str(projection).ok()?;
429 let s = serde_json::json!({ "instance": v.get("instance")?, "values": v.get("values")? });
430 canonical_value_bytes(&s).ok()
431}
432
433fn content_key(
434 backend_id: &str,
435 descriptor: &DescriptorId,
436 projection: &str,
437 principal: &str,
438) -> String {
439 let mut b = Vec::new();
440 for part in [
441 backend_id.as_bytes(),
442 descriptor.as_str().as_bytes(),
443 projection.as_bytes(),
444 principal.as_bytes(),
445 ] {
446 b.extend_from_slice(&(part.len() as u64).to_be_bytes());
447 b.extend_from_slice(part);
448 }
449 digest_bytes(&b)
450}
451
452async fn infer(state: &Arc<State>, body: &[u8]) -> Result<Vec<u8>, Refusal> {
453 let req = InferRequest::parse(body).map_err(|e| bad(e.to_string()))?;
454 if req.items.is_empty() {
455 return Err(bad("no items"));
456 }
457 let ids: BTreeSet<&str> = req.items.iter().map(|i| i.attempt_id.as_str()).collect();
458 if ids.len() != req.items.len() {
459 return Err(bad("an attempt id appears twice"));
460 }
461 if !req.members_fit_version() {
463 return Err(bad("principal reference not allowed or not canonical"));
464 }
465 let hosted = lock(&state.hosted).get(&req.backend_id).cloned();
466 let answers: Vec<(String, Recorded)> = match hosted {
467 _ if !state.config.versions.contains(&req.version) => {
468 refused_all(&req, RemoteCode::Capability, "unsupported protocol version")
469 }
470 None => refused_all(&req, RemoteCode::Capability, "no such backend"),
471 Some(h) => run_items(state, &req, &h).await?,
472 };
473 let hosted = lock(&state.hosted).get(&req.backend_id).cloned();
474 let (artifact, served) = match &hosted {
475 Some(h) => (
476 h.backend.descriptor().artifact.clone(),
477 match h.served.disclosure {
478 Disclosure::Unknown => ServedIdentity::unknown(),
479 d => ServedIdentity {
480 identity: h.served.identity.clone(),
481 disclosure: d,
482 },
483 },
484 ),
485 None => (req.artifact.clone(), ServedIdentity::unknown()),
486 };
487 let charge = sum_charges(&answers.iter().map(|(_, r)| r.charge).collect::<Vec<_>>());
488 let items: Vec<ItemAnswer> = answers
489 .into_iter()
490 .map(|(attempt_id, r)| ItemAnswer {
491 attempt_id,
492 result: r.result,
493 })
494 .collect();
495 let mut resp = InferResponse {
496 schema: schema::REMOTE_INFER.into(),
497 version: req.version,
498 artifact,
499 served,
500 route: vec![],
501 generation_id: None,
502 items,
503 charge,
504 extras: BTreeMap::new(),
505 };
506 if let Ok(b) = resp.record_canonical() {
507 return Ok(b);
508 }
509 for i in &mut resp.items {
511 if let ItemResult::Output(o) = &i.result
512 && rustev_contract::canonical::record_canonical_bytes(o).is_err()
513 {
514 i.result = failure(RemoteCode::RemoteError, "output has no JSON form");
515 }
516 }
517 resp.record_canonical()
518 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
519}
520
521enum Slot {
522 Answer(Recorded),
523 Wait(watch::Receiver<Option<Recorded>>),
524}
525
526async fn run_items(
527 state: &Arc<State>,
528 req: &InferRequest,
529 h: &Hosted,
530) -> Result<Vec<(String, Recorded)>, Refusal> {
531 let descriptor: &BackendDescriptor = h.backend.descriptor();
532 let current = descriptor
533 .id()
534 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
535 if current != req.descriptor || descriptor.artifact != req.artifact {
536 return Ok(refused_all(
537 req,
538 RemoteCode::IdentityMismatch,
539 "the hosted descriptor or artifact differs from the negotiated one",
540 ));
541 }
542 if req.items.len() > 1
550 && descriptor
551 .operations
552 .iter()
553 .any(|o| o.isolation == Some(rustev_contract::descriptor::Isolation::PerItem))
554 {
555 return Ok(refused_all(
556 req,
557 RemoteCode::Capability,
558 "a capability requires per-item isolation",
559 ));
560 }
561 let max_items = match state.config.batching {
562 Batching::None => 1,
563 Batching::SharedState { max_items } => (max_items as usize).min(MAX_ITEMS),
564 };
565 if req.items.len() > max_items {
566 return Ok(refused_all(req, RemoteCode::Capability, "too many items"));
567 }
568 if req.items.len() > 1 {
569 let first = state_bytes(&req.items[0].projection);
570 if first.is_none()
571 || req
572 .items
573 .iter()
574 .any(|i| state_bytes(&i.projection) != first)
575 {
576 return Err((
577 StatusCode::UNPROCESSABLE_ENTITY,
578 "batched items must share their values and instance".into(),
579 ));
580 }
581 }
582 let window = Duration::from_millis(state.config.idempotency_window_ms);
583 let mut slots = Vec::with_capacity(req.items.len());
584 {
585 let mut attempts = lock(&state.attempts);
586 let now = Instant::now();
587 attempts.retain(|_, e| e.finished_at.is_none_or(|t| now.duration_since(t) < window));
588 let new = req
589 .items
590 .iter()
591 .filter(|i| !attempts.contains_key(&i.attempt_id))
592 .count();
593 if attempts.len() + new > state.config.max_remembered {
594 return Err((StatusCode::SERVICE_UNAVAILABLE, "overloaded".into()));
595 }
596 let principal_digest = req
597 .principal
598 .as_ref()
599 .map(|p| p.evidence().digest)
600 .unwrap_or_default();
601 let principal_handle = req
602 .principal
603 .as_ref()
604 .and_then(|p| p.reference_bytes())
605 .unwrap_or_default();
606 for item in &req.items {
607 let content = content_key(
608 &req.backend_id,
609 &req.descriptor,
610 &item.projection,
611 &principal_digest,
612 );
613 if let Some(e) = attempts.get(&item.attempt_id) {
614 let recorded = e.done.borrow().clone();
615 slots.push(Slot::Answer(if e.content != content {
616 Recorded {
617 result: failure(RemoteCode::MalformedRequest, "attempt_id_conflict"),
618 charge: Charge::Observed { units: 0 },
619 ack: CancelAck::NotRequested,
620 remote: RemoteEnd::Finished,
621 }
622 } else {
623 match recorded {
624 Some(r) => r,
625 None => Recorded {
626 result: ItemResult::InProgress,
627 charge: Charge::Unknown,
628 ack: CancelAck::NotRequested,
629 remote: RemoteEnd::PossiblyContinuing,
630 },
631 }
632 }));
633 continue;
634 }
635 if item.projection.len() as u64 > descriptor.input_limit.max_bytes {
636 slots.push(Slot::Answer(Recorded {
637 result: failure(RemoteCode::Capability, "input exceeds the declared limit"),
638 charge: Charge::Observed { units: 0 },
639 ack: CancelAck::NotRequested,
640 remote: RemoteEnd::Finished,
641 }));
642 continue;
643 }
644 let (tx, rx) = watch::channel(None);
645 let signal = CancelSignal::new();
646 let cancel_requested = Arc::new(AtomicBool::new(false));
647 attempts.insert(
648 item.attempt_id.clone(),
649 Entry {
650 backend_id: req.backend_id.clone(),
651 content,
652 signal: signal.clone(),
653 cancel_requested: cancel_requested.clone(),
654 done: rx.clone(),
655 finished_at: None,
656 },
657 );
658 spawn_attempt(
659 state.clone(),
660 h.backend.clone(),
661 item.attempt_id.clone(),
662 item.projection.clone().into_bytes(),
663 req.budget_ms,
664 req.trace_id.clone(),
665 principal_handle.clone(),
666 signal,
667 cancel_requested,
668 tx,
669 );
670 slots.push(Slot::Wait(rx));
671 }
672 }
673 let mut out = Vec::with_capacity(slots.len());
674 for (item, slot) in req.items.iter().zip(slots) {
675 let r = match slot {
676 Slot::Answer(r) => r,
677 Slot::Wait(mut rx) => match rx.wait_for(Option::is_some).await {
678 Ok(r) => r.clone().expect("waited for Some"),
679 Err(_) => Recorded {
680 result: failure(RemoteCode::RemoteError, "the attempt vanished"),
681 charge: Charge::Unknown,
682 ack: CancelAck::NotRequested,
683 remote: RemoteEnd::PossiblyContinuing,
684 },
685 },
686 };
687 out.push((item.attempt_id.clone(), r));
688 }
689 Ok(out)
690}
691
692#[allow(clippy::too_many_arguments)]
693fn spawn_attempt(
694 state: Arc<State>,
695 backend: Arc<dyn DecisionBackend>,
696 attempt_id: String,
697 projection: Vec<u8>,
698 budget_ms: u64,
699 trace_id: String,
700 principal_handle: Vec<u8>,
701 signal: CancelSignal,
702 cancel_requested: Arc<AtomicBool>,
703 tx: watch::Sender<Option<Recorded>>,
704) {
705 let wait = Duration::from_millis(state.config.cancel_wait_ms);
706 let deadline_hit = Arc::new(AtomicBool::new(false));
707 let hit = deadline_hit.clone();
708 let requested = cancel_requested.clone();
709 let id = attempt_id.clone();
710 let outer_state = state.clone();
711 let inner = tokio::spawn(async move {
712 if budget_ms == 0 {
713 return Recorded {
714 result: failure(RemoteCode::RemoteDeadline, "no budget left; not started"),
715 charge: Charge::Observed { units: 0 },
716 ack: CancelAck::NotRequested,
717 remote: RemoteEnd::Finished,
718 };
719 }
720 state.dispatched.fetch_add(1, Ordering::SeqCst);
721 let cx = CallContext {
722 remaining_ms: DurationMs(budget_ms),
723 trace_id,
724 principal_handle,
728 };
729 let fut = backend.infer(AttemptCall {
730 projection: &projection,
731 attempt_id: &attempt_id,
732 cancel: signal.clone(),
733 cx: &cx,
734 });
735 tokio::pin!(fut);
736 let sleep = tokio::time::sleep(Duration::from_millis(budget_ms));
737 tokio::pin!(sleep);
738 let report = tokio::select! {
739 r = &mut fut => Some(r),
740 _ = &mut sleep => {
741 hit.store(true, Ordering::SeqCst);
742 signal.raise();
743 tokio::time::timeout(wait, &mut fut).await.ok()
744 }
745 };
746 match report {
747 Some(r) => map_report(
748 r,
749 hit.load(Ordering::SeqCst),
750 requested.load(Ordering::SeqCst),
751 ),
752 None => unconfirmed_deadline(),
753 }
754 });
755 tokio::spawn(async move {
756 let r = match inner.await {
757 Ok(r) => r,
758 Err(_) => Recorded {
759 result: failure(RemoteCode::RemoteError, "the hosted backend panicked"),
760 charge: Charge::Unknown,
761 ack: CancelAck::NotRequested,
762 remote: RemoteEnd::PossiblyContinuing,
763 },
764 };
765 let mut attempts = lock(&outer_state.attempts);
767 if let Some(e) = attempts.get_mut(&id) {
768 e.finished_at = Some(Instant::now());
769 }
770 let _ = tx.send(Some(r));
771 });
772}
773
774fn unconfirmed_deadline() -> Recorded {
779 Recorded {
780 result: failure(
781 RemoteCode::RemoteError,
782 "the budget ran out and the hosted backend did not confirm a stop",
783 ),
784 charge: Charge::Unknown,
785 ack: CancelAck::Unconfirmed,
786 remote: RemoteEnd::PossiblyContinuing,
787 }
788}
789
790fn map_report(r: AttemptReport, deadline_hit: bool, cancel_requested: bool) -> Recorded {
795 let result = match r.result {
796 Ok(o) => ItemResult::Output(o),
797 Err((AdapterFailure::Cancelled, d)) if deadline_hit && r.cancel == CancelAck::Stopped => {
799 failure(RemoteCode::RemoteDeadline, &d)
800 }
801 Err((AdapterFailure::Cancelled, _)) if deadline_hit => return unconfirmed_deadline(),
802 Err((AdapterFailure::Cancelled, d)) if cancel_requested => {
803 failure(RemoteCode::Cancelled, &d)
804 }
805 Err((AdapterFailure::Cancelled, d)) => failure(
806 RemoteCode::RemoteError,
807 &format!("reported cancelled without a request: {d}"),
808 ),
809 Err((class, d)) => match code_of_detail(&d) {
810 Some(c) if failure_class(c) == class && !is_local_only(c) => {
811 let rest = d
812 .strip_prefix(&format!("remote:{}", c.as_str()))
813 .unwrap_or("")
814 .trim_start_matches(": ");
815 failure(c, rest)
816 }
817 _ => failure(
818 match class {
819 AdapterFailure::Transient => RemoteCode::RemoteError,
820 AdapterFailure::Overloaded => RemoteCode::Overloaded,
821 _ => RemoteCode::MalformedRequest,
822 },
823 &d,
824 ),
825 },
826 };
827 Recorded {
828 result,
829 charge: r.charge,
830 ack: r.cancel,
831 remote: r.remote,
832 }
833}
834
835fn cancel_answer(support: CancelSupport, r: &Recorded) -> RemoteCancelAnswer {
836 let finished = r.remote == RemoteEnd::Finished && r.ack != CancelAck::Unconfirmed;
837 match (support, finished, r.charge) {
838 (CancelSupport::Confirmed, true, c) if c != Charge::Unknown => {
839 RemoteCancelAnswer::Stopped { charge: c }
840 }
841 _ => RemoteCancelAnswer::Unconfirmed,
842 }
843}
844
845async fn cancel(state: &Arc<State>, body: &[u8]) -> Result<Vec<u8>, Refusal> {
846 let req = CancelRequest::parse(body).map_err(|e| bad(e.to_string()))?;
847 let support = state.config.cancellation;
848 if !state.config.versions.contains(&req.version) || support == CancelSupport::None {
849 return Err((
850 StatusCode::UNPROCESSABLE_ENTITY,
851 "cancellation is not supported".into(),
852 ));
853 }
854 let mut waits = vec![];
855 {
856 let attempts = lock(&state.attempts);
857 for id in &req.attempt_ids {
858 match attempts.get(id) {
859 Some(e) if e.backend_id == req.backend_id => {
860 e.cancel_requested.store(true, Ordering::SeqCst);
861 e.signal.raise();
862 waits.push((id.clone(), Some(e.done.clone())));
863 }
864 _ => waits.push((id.clone(), None)),
865 }
866 }
867 }
868 let wait = Duration::from_millis(state.config.cancel_wait_ms);
869 let mut answers = vec![];
870 for (attempt_id, rx) in waits {
871 let answer = match rx {
872 None => RemoteCancelAnswer::UnknownAttempt,
873 Some(mut rx) => match tokio::time::timeout(wait, rx.wait_for(Option::is_some)).await {
874 Ok(Ok(r)) => cancel_answer(support, r.as_ref().expect("waited for Some")),
875 _ => RemoteCancelAnswer::Unconfirmed,
876 },
877 };
878 answers.push(CancelItem { attempt_id, answer });
879 }
880 CancelResponse {
881 schema: schema::REMOTE_CANCEL.into(),
882 version: req.version,
883 answers,
884 }
885 .record_canonical()
886 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 #[test]
894 fn bearer_comparison_is_exact_and_length_checked() {
895 assert!(constant_time_eq(b"Bearer abc", b"Bearer abc"));
896 assert!(!constant_time_eq(b"Bearer abc", b"Bearer abd"));
897 assert!(!constant_time_eq(b"Bearer abc", b"Bearer ab"));
898 assert!(!constant_time_eq(b"", b"x"));
899 assert!(constant_time_eq(b"", b""));
900 }
901
902 fn cancelled(ack: CancelAck, charge: Charge) -> AttemptReport {
903 AttemptReport {
904 result: Err((AdapterFailure::Cancelled, "SYNTHETIC".into())),
905 charge,
906 cancel: ack,
907 remote: RemoteEnd::Finished,
908 }
909 }
910
911 #[test]
912 fn remote_deadline_only_for_a_confirmed_stop() {
913 let stopped = map_report(
914 cancelled(CancelAck::Stopped, Charge::Observed { units: 1 }),
915 true,
916 false,
917 );
918 assert!(matches!(
919 stopped.result,
920 ItemResult::Failure {
921 code: RemoteCode::RemoteDeadline,
922 ..
923 }
924 ));
925 assert_eq!(stopped.charge, Charge::Observed { units: 1 });
926 let unconfirmed = map_report(
927 cancelled(CancelAck::Unconfirmed, Charge::Observed { units: 1 }),
928 true,
929 false,
930 );
931 assert!(matches!(
932 unconfirmed.result,
933 ItemResult::Failure {
934 code: RemoteCode::RemoteError,
935 ..
936 }
937 ));
938 assert_eq!(unconfirmed.charge, Charge::Unknown);
939 }
940
941 #[test]
942 fn a_hosted_transport_code_is_never_sent_on() {
943 let r = map_report(
944 AttemptReport {
945 result: Err((
946 AdapterFailure::Transient,
947 "remote:transport_unsent: SYNTHETIC".into(),
948 )),
949 charge: Charge::Observed { units: 3 },
950 cancel: CancelAck::NotRequested,
951 remote: RemoteEnd::Finished,
952 },
953 false,
954 false,
955 );
956 assert!(matches!(
957 r.result,
958 ItemResult::Failure {
959 code: RemoteCode::RemoteError,
960 ..
961 }
962 ));
963 assert_eq!(r.charge, Charge::Observed { units: 3 });
964 }
965}