Skip to main content

rustev_remote_http/
server.rs

1//! The `rustev.remote/1` HTTP server (spec 009, 3.10 and 3.11): hosts any
2//! `DecisionBackend` behind `POST /rustev/remote/1/{describe,infer,cancel}`.
3//!
4//! It forwards the attempt id and the budget it was told, maps the hosted
5//! backend's `AttemptReport` to the response one to one, never runs an
6//! attempt id twice within its idempotency window, and does not start work
7//! once the budget has expired. It exposes no other provider's wire format.
8
9use 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
49/// Paths of the three documents (3.11).
50pub 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/// Server-wide terms and limits.
55#[derive(Clone)]
56pub struct ServerConfig {
57    /// Required on every request when set.
58    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    /// Off by default (3.10.4).
64    pub batching: Batching,
65    /// Refusals here are never charged, so the default is `false`.
66    pub refusals_charged: bool,
67    /// Requests in flight beyond this are refused `503` (overloaded).
68    pub max_in_flight: usize,
69    /// How long a cancel waits for the hosted backend's acknowledgement.
70    pub cancel_wait_ms: u64,
71    /// Attempt ids remembered at most; beyond it, and with none expired,
72    /// requests are refused `503`.
73    pub max_remembered: usize,
74    /// The protocol major versions served; `[1]` by default. With 2, a
75    /// request may carry a principal reference (spec 020, 3.6), which the
76    /// hosted backend receives as its opaque principal handle.
77    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/// One hosted backend and what the server declares about it.
99#[derive(Clone)]
100pub struct Hosted {
101    pub backend: Arc<dyn DecisionBackend>,
102    /// Default: pinned to the backend's artifact identity, which the server
103    /// can guarantee because it hosts that backend.
104    pub served: ServedTerms,
105    /// Default: the backend's own per-call disclosure.
106    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/// What an attempt ended with, as recorded for idempotent replay.
139#[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/// The server. Clones share state.
165#[derive(Clone)]
166pub struct RemoteServer {
167    state: Arc<State>,
168}
169
170/// A running accept loop; dropping it stops accepting.
171pub 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    /// `http://<addr>`.
184    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    /// Host a backend under its descriptor's backend id, replacing any
207    /// hosted under that id. A replacement changes the descriptor clients
208    /// negotiated, which they then see as `identity_mismatch` (3.2.5).
209    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    /// Hosted-backend `infer` calls so far: remote work actually started.
215    pub fn dispatched(&self) -> u64 {
216        self.state.dispatched.load(Ordering::SeqCst)
217    }
218
219    /// Serve on `listener`. Plain HTTP is refused for a non-loopback
220    /// address unless `tls` is given (3.11).
221    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    /// Bind `127.0.0.1:0` and serve plain HTTP: the loopback conformance
259    /// setting.
260    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
279/// Byte equality whose time depends on the lengths only, not on where the
280/// first difference is: a credential is not guessable byte by byte.
281fn 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
426/// The members of a projection that are the shared state (3.10.4).
427fn 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    // A principal reference only in version 2, canonical and bounded.
462    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    // An output with no JSON form (a non-finite number) cannot be sent.
510    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    // Spec 035 3.15.3 is per backend, not per item: "A request with more
543    // than one item for a backend whose screening capability declares
544    // `isolation: per_item` fails every item with `capability` before the
545    // backend is called." Spec 044 3.7 applies the same member to every
546    // capability. The client's guard (3.15.2) is per item because it
547    // decides what to coalesce; the server refuses any multi-item request to
548    // such a backend.
549    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            // Spec 009 3.1.3: empty unless a version 2 request carried the
725            // host's opaque principal reference (spec 020, 3.6), which the
726            // hosted backend receives uninterpreted.
727            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        // The idempotency window runs from completion.
766        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
774/// The told budget ran out and the hosted backend did not confirm a stop:
775/// its work may go on, so this is not `remote_deadline` (which says the
776/// remote side stopped) but `remote_error` with charge `unknown`, which a
777/// client records as possibly continuing (spec 013).
778fn 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
790/// The hosted report, one to one: output, or the failure's own code when
791/// its detail carries one of the same class, else the class's code. Codes
792/// only a client can observe (`transport_*`) are never sent on: a hosted
793/// backend's own transport failure is a `remote_error` here.
794fn map_report(r: AttemptReport, deadline_hit: bool, cancel_requested: bool) -> Recorded {
795    let result = match r.result {
796        Ok(o) => ItemResult::Output(o),
797        // `remote_deadline` only for a stop the hosted backend confirmed.
798        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}