Skip to main content

rustev_remote_http/
callback_server.rs

1//! The host-side HTTP server of the two callback protocols (spec 042, 3.10
2//! and 3.11): hosts any `ContextSource` behind
3//! `POST /rustev/remote-context/1/{describe,fetch,cancel,invalidations}` and
4//! any `EvidenceSink` behind `POST /rustev/remote-sink/1/{describe,deliver}`.
5//!
6//! A context fetch runs an attempt id at most once inside the idempotency
7//! window: a repeat with identical content gets the recorded answer, and
8//! with other content `malformed_request` (`attempt_id_conflict`). A delivery
9//! with a decision id seen inside the deduplication window is `duplicate`
10//! for the same digest and `conflict` for another. Every table is bounded;
11//! a full one with nothing expired refuses with `503`. A receipt or a
12//! snapshot is evidence, never authorization.
13//!
14//! A source hosted with [`CallbackServer::host_acquisition`] is described
15//! with its spec 036 descriptor and serves
16//! `POST /rustev/remote-context/1/acquire` under the same attempt rules; the
17//! page bytes the hosted `AcquisitionSource` returns are sent unchanged.
18
19use std::collections::{BTreeMap, HashMap, VecDeque};
20use std::convert::Infallible;
21use std::io;
22use std::net::SocketAddr;
23use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
24use std::sync::{Arc, Mutex};
25use std::time::Duration;
26
27use http_body_util::{BodyExt, Full, Limited};
28use hyper::body::{Bytes, Incoming};
29use hyper::header::{AUTHORIZATION, CONTENT_TYPE};
30use hyper::service::service_fn;
31use hyper::{Method, Request, Response, StatusCode};
32use hyper_util::rt::TokioIo;
33use rustev_contract::Document;
34use rustev_contract::ids::ContentDigest;
35use rustev_contract::remote::{CancelSupport, MAX_TEXT_BYTES, RemoteCode, cut_to};
36use rustev_contract::remote_context::{
37    self as rc, AcquireCode, AcquireResult, CancelAnswer, CancelItem, ContextCode, ContextField,
38    ContextTerms, EndpointClass, FetchResult, HostedDescriptor, HostedSource, Invalidation,
39    InvalidationKind, InvalidationScope, InvalidationSupport, PageBytes,
40};
41use rustev_contract::remote_sink::{self as rs, DeliveryResult, RecordSchema};
42use rustev_contract::run::{Charge, RunRecord};
43use rustev_contract::time::DurationMs;
44use rustev_core::seams::{
45    AcquireCall, AcquisitionFailure, AcquisitionSource, CallContext, CancelSignal, ContextSource,
46    EvidenceSink,
47};
48use tokio::net::TcpListener;
49use tokio::sync::watch;
50use tokio::task::JoinHandle;
51use tokio::time::Instant;
52
53use crate::http::Secret;
54use crate::taxonomy::code_of_detail;
55use crate::wire::digest_bytes;
56
57pub const CONTEXT_DESCRIBE_PATH: &str = "/rustev/remote-context/1/describe";
58pub const CONTEXT_FETCH_PATH: &str = "/rustev/remote-context/1/fetch";
59pub const CONTEXT_CANCEL_PATH: &str = "/rustev/remote-context/1/cancel";
60pub const CONTEXT_INVALIDATIONS_PATH: &str = "/rustev/remote-context/1/invalidations";
61pub const CONTEXT_ACQUIRE_PATH: &str = "/rustev/remote-context/1/acquire";
62pub const SINK_DESCRIBE_PATH: &str = "/rustev/remote-sink/1/describe";
63pub const SINK_DELIVER_PATH: &str = "/rustev/remote-sink/1/deliver";
64
65/// Server-wide terms and bounds of the context protocol.
66#[derive(Clone)]
67pub struct CallbackConfig {
68    /// Required on every request when set.
69    pub bearer: Option<Secret>,
70    pub idempotency_window_ms: u64,
71    pub max_request_bytes: u64,
72    pub max_response_bytes: u64,
73    pub cancellation: CancelSupport,
74    pub invalidation: InvalidationSupport,
75    /// Requests in flight beyond this are refused `503`.
76    pub max_in_flight: usize,
77    /// Attempt ids and decision ids remembered at most each; beyond it, with
78    /// none expired, requests are refused `503`.
79    pub max_remembered: usize,
80    /// Invalidation items retained for polling; older ones are dropped, which
81    /// a poller sees as a sequence gap.
82    pub max_invalidations: usize,
83}
84
85impl Default for CallbackConfig {
86    fn default() -> Self {
87        CallbackConfig {
88            bearer: None,
89            idempotency_window_ms: 10 * 60 * 1000,
90            max_request_bytes: 1024 * 1024,
91            max_response_bytes: 4 * 1024 * 1024,
92            cancellation: CancelSupport::Confirmed,
93            invalidation: InvalidationSupport::None,
94            max_in_flight: 64,
95            max_remembered: 10_000,
96            max_invalidations: 10_000,
97        }
98    }
99}
100
101/// One hosted context source and the fields it declares.
102#[derive(Clone)]
103pub struct HostedContext {
104    pub source: Arc<dyn ContextSource>,
105    pub fields: Vec<ContextField>,
106}
107
108/// The hosted evidence sink and what it declares.
109#[derive(Clone)]
110pub struct HostedSink {
111    pub sink: Arc<dyn EvidenceSink>,
112    pub record_schemas: Vec<RecordSchema>,
113    /// The one durability state an accepted delivery names; the sink's
114    /// receipt becomes the external id.
115    pub durability: String,
116    pub deduplication_window_ms: u64,
117    pub max_record_bytes: u64,
118}
119
120#[derive(Clone)]
121struct HostedAcquisition {
122    source: Arc<dyn AcquisitionSource>,
123    descriptor: HostedDescriptor,
124}
125
126/// One remembered attempt: a fetch or an acquisition.
127struct Fetch {
128    content: String,
129    cancel: watch::Sender<bool>,
130    done: watch::Receiver<Option<Vec<u8>>>,
131    finished_at: Option<Instant>,
132}
133
134struct Delivery {
135    digest: ContentDigest,
136    done: watch::Receiver<Option<DeliveryResult>>,
137    finished_at: Option<Instant>,
138}
139
140#[derive(Default)]
141struct InvalidationLog {
142    last: u64,
143    items: VecDeque<Invalidation>,
144}
145
146struct State {
147    config: CallbackConfig,
148    contexts: Mutex<BTreeMap<String, HostedContext>>,
149    acquisitions: Mutex<BTreeMap<String, HostedAcquisition>>,
150    sink: Mutex<Option<HostedSink>>,
151    fetches: Mutex<HashMap<String, Fetch>>,
152    deliveries: Mutex<HashMap<String, Delivery>>,
153    invalidations: Mutex<InvalidationLog>,
154    in_flight: AtomicUsize,
155    dispatched: AtomicU64,
156}
157
158/// The server. Clones share state.
159#[derive(Clone)]
160pub struct CallbackServer {
161    state: Arc<State>,
162}
163
164/// A running accept loop; dropping it stops accepting.
165pub struct CallbackHandle {
166    pub addr: SocketAddr,
167    task: JoinHandle<()>,
168}
169
170impl Drop for CallbackHandle {
171    fn drop(&mut self) {
172        self.task.abort();
173    }
174}
175
176impl CallbackHandle {
177    /// `http://<addr>`.
178    pub fn url(&self) -> String {
179        format!("http://{}", self.addr)
180    }
181}
182
183fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
184    m.lock().unwrap_or_else(|e| e.into_inner())
185}
186
187type Refusal = (StatusCode, String);
188
189fn bad(why: impl Into<String>) -> Refusal {
190    (StatusCode::BAD_REQUEST, why.into())
191}
192
193fn internal(why: impl ToString) -> Refusal {
194    (StatusCode::INTERNAL_SERVER_ERROR, why.to_string())
195}
196
197fn overloaded() -> Refusal {
198    (StatusCode::SERVICE_UNAVAILABLE, "overloaded".into())
199}
200
201impl CallbackServer {
202    pub fn new(config: CallbackConfig) -> Self {
203        CallbackServer {
204            state: Arc::new(State {
205                config,
206                contexts: Mutex::new(BTreeMap::new()),
207                acquisitions: Mutex::new(BTreeMap::new()),
208                sink: Mutex::new(None),
209                fetches: Mutex::new(HashMap::new()),
210                deliveries: Mutex::new(HashMap::new()),
211                invalidations: Mutex::new(InvalidationLog::default()),
212                in_flight: AtomicUsize::new(0),
213                dispatched: AtomicU64::new(0),
214            }),
215        }
216    }
217
218    /// Host a context source under `source_id`, replacing any hosted there.
219    /// A replacement changes the describe digest, which clients that
220    /// described before then see as `describe_mismatch`.
221    pub fn host_context(&self, source_id: &str, hosted: HostedContext) -> Result<(), String> {
222        if source_id.is_empty() || source_id.len() > MAX_TEXT_BYTES {
223            return Err("source id must be 1 to 256 bytes".into());
224        }
225        lock(&self.state.contexts).insert(source_id.into(), hosted);
226        Ok(())
227    }
228
229    /// Serve acquisitions for the context source hosted under `source_id`,
230    /// replacing any served there: describe then carries the source's
231    /// descriptor with its `SourceId` (3.10.1), so the describe digest
232    /// changes. The context source must be hosted first, and no other hosted
233    /// source may carry the same `SourceId`.
234    pub fn host_acquisition(
235        &self,
236        source_id: &str,
237        source: Arc<dyn AcquisitionSource>,
238    ) -> Result<(), String> {
239        if !lock(&self.state.contexts).contains_key(source_id) {
240            return Err("no context source is hosted under that id".into());
241        }
242        let descriptor = HostedDescriptor::new(source.descriptor()).map_err(|e| e.to_string())?;
243        if descriptor
244            .document
245            .document()
246            .map_err(|e| e.to_string())?
247            .operations
248            .is_none()
249        {
250            return Err("the source descriptor declares no operation".into());
251        }
252        let mut acquisitions = lock(&self.state.acquisitions);
253        if acquisitions
254            .iter()
255            .any(|(id, a)| id != source_id && a.descriptor.id == descriptor.id)
256        {
257            return Err("another hosted source carries that SourceId".into());
258        }
259        acquisitions.insert(source_id.into(), HostedAcquisition { source, descriptor });
260        Ok(())
261    }
262
263    /// Host the evidence sink, replacing any hosted before.
264    pub fn host_sink(&self, hosted: HostedSink) -> Result<(), String> {
265        if hosted.record_schemas.is_empty()
266            || hosted.durability.is_empty()
267            || hosted.durability.len() > 128
268            || !hosted
269                .durability
270                .bytes()
271                .all(|c| c.is_ascii_alphanumeric() || b"_-".contains(&c))
272            || hosted.max_record_bytes == 0
273        {
274            return Err("sink needs a record schema, a durability state and a byte bound".into());
275        }
276        *lock(&self.state.sink) = Some(hosted);
277        Ok(())
278    }
279
280    /// Hosted calls started so far: fetches, acquisitions and deliveries.
281    pub fn dispatched(&self) -> u64 {
282        self.state.dispatched.load(Ordering::SeqCst)
283    }
284
285    /// The digest of the describe answer clients receive now.
286    pub fn describe_digest(&self) -> Result<ContentDigest, String> {
287        describe_response(&self.state)
288            .record_digest()
289            .map_err(|e| e.to_string())
290    }
291
292    /// Record an invalidation for pollers (3.10.6) and return its sequence
293    /// number, strictly increasing across every source this server hosts.
294    pub fn publish_invalidation(
295        &self,
296        source_id: &str,
297        kind: InvalidationKind,
298        fields: Vec<String>,
299        scope: InvalidationScope,
300    ) -> Result<u64, String> {
301        if !lock(&self.state.contexts).contains_key(source_id) {
302            return Err("unknown source".into());
303        }
304        let mut log = lock(&self.state.invalidations);
305        let item = Invalidation {
306            schema: rc::INVALIDATION.into(),
307            source_id: source_id.into(),
308            sequence: log.last + 1,
309            kind,
310            fields,
311            scope,
312        };
313        Invalidation::parse(&item.record_canonical().map_err(|e| e.to_string())?)
314            .map_err(|e| e.to_string())?;
315        log.last = item.sequence;
316        log.items.push_back(item);
317        while log.items.len() > self.state.config.max_invalidations {
318            log.items.pop_front();
319        }
320        Ok(log.last)
321    }
322
323    /// Serve on `listener`. Plain HTTP is refused for a non-loopback
324    /// address unless `tls` is given (3.10).
325    pub fn serve(
326        &self,
327        listener: TcpListener,
328        tls: Option<Arc<rustls::ServerConfig>>,
329    ) -> io::Result<CallbackHandle> {
330        let addr = listener.local_addr()?;
331        if tls.is_none() && !addr.ip().is_loopback() {
332            return Err(io::Error::new(
333                io::ErrorKind::InvalidInput,
334                "TLS is required except for loopback",
335            ));
336        }
337        let acceptor = tls.map(tokio_rustls::TlsAcceptor::from);
338        let state = self.state.clone();
339        let task = tokio::spawn(async move {
340            loop {
341                let Ok((stream, _)) = listener.accept().await else {
342                    continue;
343                };
344                let _ = stream.set_nodelay(true);
345                let state = state.clone();
346                let acceptor = acceptor.clone();
347                tokio::spawn(async move {
348                    match acceptor {
349                        None => serve_conn(state, stream).await,
350                        Some(a) => {
351                            if let Ok(tls) = a.accept(stream).await {
352                                serve_conn(state, tls).await
353                            }
354                        }
355                    }
356                });
357            }
358        });
359        Ok(CallbackHandle { addr, task })
360    }
361
362    /// Serve a Unix socket listener; its file permissions authenticate.
363    #[cfg(unix)]
364    pub fn serve_unix(&self, listener: tokio::net::UnixListener) -> CallbackHandle {
365        let state = self.state.clone();
366        let task = tokio::spawn(async move {
367            loop {
368                let Ok((stream, _)) = listener.accept().await else {
369                    continue;
370                };
371                tokio::spawn(serve_conn(state.clone(), stream));
372            }
373        });
374        CallbackHandle {
375            addr: SocketAddr::from(([127, 0, 0, 1], 0)),
376            task,
377        }
378    }
379
380    /// Bind `127.0.0.1:0` and serve plain HTTP: the loopback setting.
381    pub async fn serve_loopback(&self) -> io::Result<CallbackHandle> {
382        let listener = TcpListener::bind(("127.0.0.1", 0)).await?;
383        self.serve(listener, None)
384    }
385}
386
387async fn serve_conn<S>(state: Arc<State>, stream: S)
388where
389    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
390{
391    let svc = service_fn(move |req| {
392        let state = state.clone();
393        async move { Ok::<_, Infallible>(handle(state, req).await) }
394    });
395    let _ = hyper::server::conn::http1::Builder::new()
396        .serve_connection(TokioIo::new(stream), svc)
397        .await;
398}
399
400/// Byte equality whose time depends on the lengths only.
401fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
402    a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
403}
404
405fn reply(status: StatusCode, body: Vec<u8>) -> Response<Full<Bytes>> {
406    let mut r = Response::new(Full::new(Bytes::from(body)));
407    *r.status_mut() = status;
408    r.headers_mut()
409        .insert(CONTENT_TYPE, "application/json".parse().expect("static"));
410    r
411}
412
413fn refuse(status: StatusCode, why: &str) -> Response<Full<Bytes>> {
414    let body = serde_json::json!({ "error": cut_to(why, MAX_TEXT_BYTES) });
415    reply(status, serde_json::to_vec(&body).unwrap_or_default())
416}
417
418struct InFlight<'a>(&'a AtomicUsize);
419
420impl Drop for InFlight<'_> {
421    fn drop(&mut self) {
422        self.0.fetch_sub(1, Ordering::SeqCst);
423    }
424}
425
426async fn handle(state: Arc<State>, req: Request<Incoming>) -> Response<Full<Bytes>> {
427    if let Some(b) = &state.config.bearer {
428        let expected = format!("Bearer {}", b.expose());
429        let ok = req
430            .headers()
431            .get(AUTHORIZATION)
432            .is_some_and(|v| constant_time_eq(v.as_bytes(), expected.as_bytes()));
433        if !ok {
434            return refuse(StatusCode::UNAUTHORIZED, "credentials refused");
435        }
436    }
437    if req.method() != Method::POST {
438        return refuse(StatusCode::METHOD_NOT_ALLOWED, "POST only");
439    }
440    let n = state.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
441    let _guard = InFlight(&state.in_flight);
442    if n > state.config.max_in_flight {
443        return refuse(StatusCode::SERVICE_UNAVAILABLE, "overloaded");
444    }
445    let path = req.uri().path().to_string();
446    let max = match path.as_str() {
447        // A delivery's bound is the sink's record bound plus its envelope.
448        SINK_DELIVER_PATH => rs::DELIVER_LIMITS.max_bytes,
449        _ => state.config.max_request_bytes as usize,
450    };
451    let body = match Limited::new(req.into_body(), max).collect().await {
452        Ok(b) => b.to_bytes(),
453        Err(_) => return refuse(StatusCode::PAYLOAD_TOO_LARGE, "request too large"),
454    };
455    let r = match path.as_str() {
456        CONTEXT_DESCRIBE_PATH => context_describe(&state, &body),
457        CONTEXT_FETCH_PATH => fetch(&state, &body).await,
458        CONTEXT_CANCEL_PATH => cancel(&state, &body),
459        CONTEXT_INVALIDATIONS_PATH => invalidations(&state, &body),
460        CONTEXT_ACQUIRE_PATH => acquire(&state, &body).await,
461        SINK_DESCRIBE_PATH => sink_describe(&state, &body),
462        SINK_DELIVER_PATH => deliver(&state, &body).await,
463        _ => return refuse(StatusCode::NOT_FOUND, "no such document"),
464    };
465    match r {
466        Ok(bytes) => reply(StatusCode::OK, bytes),
467        Err((status, why)) => refuse(status, &why),
468    }
469}
470
471fn describe_response(state: &State) -> rc::DescribeResponse {
472    let c = &state.config;
473    let acquisitions = lock(&state.acquisitions);
474    rc::DescribeResponse {
475        schema: rc::DESCRIBE.into(),
476        version: 1,
477        sources: lock(&state.contexts)
478            .iter()
479            .map(|(id, h)| HostedSource {
480                source_id: id.clone(),
481                fields: h.fields.clone(),
482                terms: ContextTerms {
483                    cancellation: c.cancellation,
484                    idempotency_window_ms: c.idempotency_window_ms,
485                    max_request_bytes: c.max_request_bytes,
486                    max_response_bytes: c.max_response_bytes,
487                    invalidation: c.invalidation.clone(),
488                    endpoint_class: EndpointClass::HostOperated,
489                },
490                descriptor: acquisitions.get(id).map(|a| a.descriptor.clone()),
491            })
492            .collect(),
493    }
494}
495
496fn context_describe(state: &State, body: &[u8]) -> Result<Vec<u8>, Refusal> {
497    let req = rc::DescribeRequest::parse(body).map_err(|e| bad(e.to_string()))?;
498    if !req.versions.contains(&1) {
499        return Err(bad("no common protocol version"));
500    }
501    describe_response(state)
502        .record_canonical()
503        .map_err(internal)
504}
505
506fn fetch_failure(code: RemoteCode, detail: &str) -> Result<Vec<u8>, Refusal> {
507    rc::FetchResponse {
508        schema: rc::FETCH.into(),
509        result: FetchResult::Failure {
510            code: ContextCode(code),
511            detail: cut_to(detail, MAX_TEXT_BYTES),
512        },
513    }
514    .record_canonical()
515    .map_err(internal)
516}
517
518async fn fetch(state: &Arc<State>, body: &[u8]) -> Result<Vec<u8>, Refusal> {
519    let req = rc::FetchRequest::parse(body).map_err(|e| bad(e.to_string()))?;
520    if req.version != 1 {
521        return fetch_failure(RemoteCode::Capability, "unsupported version");
522    }
523    let Some(hosted) = lock(&state.contexts).get(&req.source_id).cloned() else {
524        return fetch_failure(RemoteCode::Capability, "unknown source");
525    };
526    let digest = describe_response(state).record_digest().map_err(internal)?;
527    if req.describe_digest != digest {
528        return fetch_failure(RemoteCode::IdentityMismatch, "describe digest differs");
529    }
530    // The content an attempt id is bound to: every member but the budget,
531    // which shrinks on a legitimate repeat.
532    let content = {
533        let mut keyed = req.clone();
534        keyed.budget_ms = 0;
535        digest_bytes(&keyed.record_canonical().map_err(internal)?)
536    };
537    let Some(principal) = req.principal.reference_bytes() else {
538        return Err(bad("invalid principal reference"));
539    };
540    let (attempt_id, budget_ms) = (req.attempt_id.clone(), req.budget_ms);
541    once(
542        state,
543        &attempt_id,
544        content,
545        budget_ms,
546        fetch_failure,
547        |cancel, tx| {
548            tokio::spawn(run_fetch(state.clone(), hosted, req, principal, cancel, tx));
549        },
550    )
551    .await
552}
553
554/// Starts an attempt id at most once inside the idempotency window and
555/// waits for its answer: a repeat with the same content waits for the
556/// recorded answer, with other content it is `attempt_id_conflict`.
557async fn once(
558    state: &Arc<State>,
559    attempt_id: &str,
560    content: String,
561    budget_ms: u64,
562    failure: fn(RemoteCode, &str) -> Result<Vec<u8>, Refusal>,
563    start: impl FnOnce(watch::Receiver<bool>, watch::Sender<Option<Vec<u8>>>),
564) -> Result<Vec<u8>, Refusal> {
565    let window = Duration::from_millis(state.config.idempotency_window_ms);
566    let mut done = {
567        let mut fetches = lock(&state.fetches);
568        let now = Instant::now();
569        fetches.retain(|_, e| e.finished_at.is_none_or(|t| now.duration_since(t) < window));
570        if let Some(e) = fetches.get(attempt_id) {
571            if e.content != content {
572                return failure(RemoteCode::MalformedRequest, "attempt_id_conflict");
573            }
574            e.done.clone()
575        } else {
576            if budget_ms == 0 {
577                return failure(RemoteCode::RemoteDeadline, "budget expired");
578            }
579            if fetches.len() >= state.config.max_remembered {
580                return Err(overloaded());
581            }
582            let (tx, rx) = watch::channel(None);
583            let (cancel_tx, cancel_rx) = watch::channel(false);
584            fetches.insert(
585                attempt_id.into(),
586                Fetch {
587                    content,
588                    cancel: cancel_tx,
589                    done: rx.clone(),
590                    finished_at: None,
591                },
592            );
593            state.dispatched.fetch_add(1, Ordering::SeqCst);
594            start(cancel_rx, tx);
595            rx
596        }
597    };
598    let answer = done
599        .wait_for(Option::is_some)
600        .await
601        .map_err(|_| internal("the attempt vanished"))?
602        .clone()
603        .expect("waited for Some");
604    Ok(answer)
605}
606
607fn finish(state: &State, attempt_id: &str, done: watch::Sender<Option<Vec<u8>>>, bytes: Vec<u8>) {
608    if let Some(e) = lock(&state.fetches).get_mut(attempt_id) {
609        e.finished_at = Some(Instant::now());
610    }
611    let _ = done.send(Some(bytes));
612}
613
614fn acquire_failure(code: AcquireCode, detail: &str, charge: Charge) -> Result<Vec<u8>, Refusal> {
615    rc::AcquireResponse {
616        schema: rc::ACQUIRE.into(),
617        result: AcquireResult::Failure {
618            code,
619            detail: cut_to(detail, MAX_TEXT_BYTES),
620        },
621        charge,
622    }
623    .record_canonical()
624    .map_err(internal)
625}
626
627fn acquire_refusal(code: RemoteCode, detail: &str) -> Result<Vec<u8>, Refusal> {
628    acquire_failure(code.into(), detail, Charge::Observed { units: 0 })
629}
630
631async fn acquire(state: &Arc<State>, body: &[u8]) -> Result<Vec<u8>, Refusal> {
632    let req = rc::AcquireRequest::parse(body).map_err(|e| bad(e.to_string()))?;
633    if req.version != 1 {
634        return acquire_refusal(RemoteCode::Capability, "unsupported version");
635    }
636    let hosted = lock(&state.acquisitions)
637        .values()
638        .find(|a| a.descriptor.id == req.source)
639        .cloned();
640    let Some(hosted) = hosted else {
641        return acquire_refusal(RemoteCode::Capability, "unknown source");
642    };
643    let declared = hosted
644        .source
645        .descriptor()
646        .operations
647        .iter()
648        .flatten()
649        .any(|o| o.name == req.operation);
650    if !declared {
651        return acquire_refusal(RemoteCode::Capability, "unknown operation");
652    }
653    let content = {
654        let mut keyed = req.clone();
655        keyed.budget_ms = 0;
656        digest_bytes(&keyed.record_canonical().map_err(internal)?)
657    };
658    let Some(principal) = req.principal.reference_bytes() else {
659        return Err(bad("invalid principal reference"));
660    };
661    let (attempt_id, budget_ms) = (req.attempt_id.clone(), req.budget_ms);
662    once(
663        state,
664        &attempt_id,
665        content,
666        budget_ms,
667        acquire_refusal,
668        |cancel, tx| {
669            tokio::spawn(run_acquire(
670                state.clone(),
671                hosted,
672                req,
673                principal,
674                cancel,
675                tx,
676            ));
677        },
678    )
679    .await
680}
681
682/// The code an acquisition failure is sent with: `denied` as itself, a
683/// detail naming a code of the failure's class keeps it, and otherwise the
684/// class's own code.
685fn acquire_code(class: AcquisitionFailure, detail: &str) -> AcquireCode {
686    let default = match class {
687        AcquisitionFailure::Denied => return AcquireCode::Denied,
688        AcquisitionFailure::Transient => RemoteCode::RemoteError,
689        AcquisitionFailure::Overloaded => RemoteCode::Overloaded,
690        AcquisitionFailure::Permanent => RemoteCode::Capability,
691    };
692    let class_of = |code| crate::taxonomy::failure_class(code);
693    code_of_detail(detail)
694        .filter(|c| class_of(*c) == class_of(default) && !crate::taxonomy::is_local_only(*c))
695        .unwrap_or(default)
696        .into()
697}
698
699async fn run_acquire(
700    state: Arc<State>,
701    hosted: HostedAcquisition,
702    req: rc::AcquireRequest,
703    principal: Vec<u8>,
704    mut cancel: watch::Receiver<bool>,
705    done: watch::Sender<Option<Vec<u8>>>,
706) {
707    let cx = CallContext {
708        remaining_ms: DurationMs(req.budget_ms),
709        trace_id: req.trace_id.clone(),
710        principal_handle: principal,
711    };
712    let bytes = match req.request.document() {
713        Err(e) => acquire_failure(
714            RemoteCode::RemoteError.into(),
715            &e.to_string(),
716            Charge::Unknown,
717        ),
718        Ok(request) => {
719            let signal = CancelSignal::new();
720            let call = AcquireCall {
721                operation: &req.operation,
722                request: &request,
723                request_id: &req.request_id,
724                attempt_id: &req.attempt_id,
725                cancel: signal.child(),
726                cx: &cx,
727            };
728            let budget = Duration::from_millis(req.budget_ms);
729            let report = tokio::select! {
730                r = tokio::time::timeout(budget, hosted.source.acquire(call)) => r.ok(),
731                _ = cancel.wait_for(|c| *c) => {
732                    signal.raise();
733                    None
734                }
735            };
736            match report {
737                Some(report) => match report.result {
738                    Ok(page) => match PageBytes::new(&page) {
739                        Ok(page) => rc::AcquireResponse {
740                            schema: rc::ACQUIRE.into(),
741                            result: AcquireResult::Page(page),
742                            charge: report.charge,
743                        }
744                        .record_canonical()
745                        .map_err(internal)
746                        .and_then(|b| {
747                            if b.len() as u64 <= state.config.max_response_bytes {
748                                Ok(b)
749                            } else {
750                                acquire_failure(
751                                    RemoteCode::RemoteError.into(),
752                                    "response exceeds its bound",
753                                    report.charge,
754                                )
755                            }
756                        }),
757                        Err(e) => acquire_failure(
758                            RemoteCode::RemoteError.into(),
759                            &e.to_string(),
760                            report.charge,
761                        ),
762                    },
763                    Err((class, detail)) => {
764                        acquire_failure(acquire_code(class, &detail), &detail, report.charge)
765                    }
766                },
767                None if *cancel.borrow() => {
768                    acquire_failure(RemoteCode::Cancelled.into(), "cancelled", Charge::Unknown)
769                }
770                None => acquire_failure(
771                    RemoteCode::RemoteDeadline.into(),
772                    "budget expired",
773                    Charge::Unknown,
774                ),
775            }
776        }
777    };
778    finish(&state, &req.attempt_id, done, bytes.unwrap_or_default());
779}
780
781async fn run_fetch(
782    state: Arc<State>,
783    hosted: HostedContext,
784    req: rc::FetchRequest,
785    principal: Vec<u8>,
786    mut cancel: watch::Receiver<bool>,
787    done: watch::Sender<Option<Vec<u8>>>,
788) {
789    let cx = CallContext {
790        remaining_ms: DurationMs(req.budget_ms),
791        trace_id: req.trace_id.clone(),
792        principal_handle: principal,
793    };
794    let budget = Duration::from_millis(req.budget_ms);
795    let result = tokio::select! {
796        r = tokio::time::timeout(budget, hosted.source.fetch(&req.fields, &cx)) => r.ok(),
797        _ = cancel.wait_for(|c| *c) => None,
798    };
799    let cancelled = *cancel.borrow();
800    let bytes = match result {
801        Some(Ok(snapshot)) => {
802            let bytes = rc::FetchResponse {
803                schema: rc::FETCH.into(),
804                result: FetchResult::Snapshot(snapshot),
805            }
806            .record_canonical();
807            match bytes {
808                Ok(b) if b.len() as u64 <= state.config.max_response_bytes => Ok(b),
809                Ok(_) => fetch_failure(RemoteCode::RemoteError, "response exceeds its bound"),
810                Err(e) => fetch_failure(RemoteCode::RemoteError, &e.to_string()),
811            }
812        }
813        Some(Err(detail)) => {
814            let code = code_of_detail(&detail).unwrap_or(RemoteCode::RemoteError);
815            fetch_failure(code, &detail)
816        }
817        None if cancelled => fetch_failure(RemoteCode::Cancelled, "cancelled"),
818        None => fetch_failure(RemoteCode::RemoteDeadline, "budget expired"),
819    };
820    finish(&state, &req.attempt_id, done, bytes.unwrap_or_default());
821}
822
823fn cancel(state: &State, body: &[u8]) -> Result<Vec<u8>, Refusal> {
824    let req = rc::CancelRequest::parse(body).map_err(|e| bad(e.to_string()))?;
825    if req.attempt_ids.len() > 1024 {
826        return Err(bad("too many attempt ids"));
827    }
828    let fetches = lock(&state.fetches);
829    let answers = req
830        .attempt_ids
831        .iter()
832        .map(|id| {
833            let answer = match fetches.get(id) {
834                None => CancelAnswer::UnknownAttempt,
835                Some(e) if e.finished_at.is_some() => CancelAnswer::Unconfirmed,
836                Some(e) => match state.config.cancellation {
837                    CancelSupport::None => CancelAnswer::Unconfirmed,
838                    CancelSupport::BestEffort => {
839                        let _ = e.cancel.send(true);
840                        CancelAnswer::Unconfirmed
841                    }
842                    // Dropping the hosted fetch's future stops it here.
843                    CancelSupport::Confirmed => {
844                        let _ = e.cancel.send(true);
845                        CancelAnswer::Stopped
846                    }
847                },
848            };
849            CancelItem {
850                attempt_id: id.clone(),
851                answer,
852            }
853        })
854        .collect();
855    rc::CancelResponse {
856        schema: rc::CANCEL.into(),
857        answers,
858    }
859    .record_canonical()
860    .map_err(internal)
861}
862
863fn invalidations(state: &State, body: &[u8]) -> Result<Vec<u8>, Refusal> {
864    let req = rc::InvalidationsRequest::parse(body).map_err(|e| bad(e.to_string()))?;
865    if !matches!(state.config.invalidation, InvalidationSupport::Poll { .. }) {
866        return Err(bad("invalidation polling is not offered"));
867    }
868    let log = lock(&state.invalidations);
869    let (sequence, items) = match req.sequence {
870        // The baseline poll: the current sequence number and no items.
871        None => (log.last, vec![]),
872        Some(s) if s > log.last => return Err(bad("sequence beyond the last published")),
873        Some(s) => {
874            // At most 256 items per answer; the poller asks again from the
875            // last one. Items dropped from the log show as a gap.
876            let items: Vec<_> = log
877                .items
878                .iter()
879                .filter(|i| i.sequence > s)
880                .take(256)
881                .cloned()
882                .collect();
883            (items.last().map_or(s, |i| i.sequence), items)
884        }
885    };
886    rc::InvalidationsResponse {
887        schema: rc::INVALIDATIONS.into(),
888        sequence,
889        items,
890    }
891    .record_canonical()
892    .map_err(internal)
893}
894
895fn sink_describe(state: &State, body: &[u8]) -> Result<Vec<u8>, Refusal> {
896    let req = rs::DescribeRequest::parse(body).map_err(|e| bad(e.to_string()))?;
897    if !req.versions.contains(&1) {
898        return Err(bad("no common protocol version"));
899    }
900    let Some(h) = lock(&state.sink).clone() else {
901        return Err((StatusCode::NOT_FOUND, "no sink is hosted".into()));
902    };
903    rs::DescribeResponse {
904        schema: rs::DESCRIBE.into(),
905        version: 1,
906        record_schemas: h.record_schemas,
907        durability_states: vec![h.durability],
908        deduplication_window_ms: h.deduplication_window_ms,
909        max_record_bytes: h.max_record_bytes,
910    }
911    .record_canonical()
912    .map_err(internal)
913}
914
915fn deliver_answer(result: DeliveryResult) -> Result<Vec<u8>, Refusal> {
916    rs::DeliverResponse {
917        schema: rs::DELIVER.into(),
918        result,
919    }
920    .record_canonical()
921    .map_err(internal)
922}
923
924async fn deliver(state: &Arc<State>, body: &[u8]) -> Result<Vec<u8>, Refusal> {
925    let Some(h) = lock(&state.sink).clone() else {
926        return Err((StatusCode::NOT_FOUND, "no sink is hosted".into()));
927    };
928    let req = rs::DeliverRequest::parse(body).map_err(|e| bad(e.to_string()))?;
929    if req.version != 1 || !h.record_schemas.contains(&req.record_schema) {
930        return deliver_answer(DeliveryResult::Rejected {
931            code: RemoteCode::Capability,
932        });
933    }
934    if req.record.len() as u64 > h.max_record_bytes {
935        return deliver_answer(DeliveryResult::Rejected {
936            code: RemoteCode::Capability,
937        });
938    }
939    let window = Duration::from_millis(h.deduplication_window_ms);
940    let key = req.decision_id.as_str().to_string();
941    enum Next {
942        Wait(watch::Receiver<Option<DeliveryResult>>),
943        Run(Box<RunRecord>, watch::Sender<Option<DeliveryResult>>),
944    }
945    let next = {
946        let mut deliveries = lock(&state.deliveries);
947        let now = Instant::now();
948        deliveries.retain(|_, e| e.finished_at.is_none_or(|t| now.duration_since(t) < window));
949        match deliveries.get(&key) {
950            Some(e) if e.digest != req.record_digest => {
951                return deliver_answer(DeliveryResult::Conflict);
952            }
953            Some(e) => Next::Wait(e.done.clone()),
954            None => {
955                if deliveries.len() >= state.config.max_remembered {
956                    return Err(overloaded());
957                }
958                let record =
959                    RunRecord::parse(req.record.as_bytes()).map_err(|e| bad(e.to_string()))?;
960                let (tx, rx) = watch::channel(None);
961                deliveries.insert(
962                    key.clone(),
963                    Delivery {
964                        digest: req.record_digest.clone(),
965                        done: rx,
966                        finished_at: None,
967                    },
968                );
969                Next::Run(Box::new(record), tx)
970            }
971        }
972    };
973    let mut done = match next {
974        Next::Wait(rx) => rx,
975        Next::Run(record, tx) => {
976            state.dispatched.fetch_add(1, Ordering::SeqCst);
977            let result = match h.sink.deliver(&record).await {
978                Ok(receipt) => DeliveryResult::Accepted {
979                    durability: h.durability.clone(),
980                    external_id: cut_to(&receipt, MAX_TEXT_BYTES),
981                    accepted_digest: req.record_digest.clone(),
982                },
983                Err(detail) => DeliveryResult::Rejected {
984                    code: code_of_detail(&detail).unwrap_or(RemoteCode::RemoteError),
985                },
986            };
987            {
988                let mut deliveries = lock(&state.deliveries);
989                if matches!(result, DeliveryResult::Rejected { .. }) {
990                    // Nothing was accepted: a later delivery may try again.
991                    deliveries.remove(&key);
992                } else if let Some(e) = deliveries.get_mut(&key) {
993                    e.finished_at = Some(Instant::now());
994                }
995            }
996            let _ = tx.send(Some(result.clone()));
997            return deliver_answer(result);
998        }
999    };
1000    let first = done
1001        .wait_for(Option::is_some)
1002        .await
1003        .map_err(|_| internal("the delivery vanished"))?
1004        .clone()
1005        .expect("waited for Some");
1006    deliver_answer(match first {
1007        DeliveryResult::Accepted {
1008            accepted_digest, ..
1009        }
1010        | DeliveryResult::Duplicate { accepted_digest } => {
1011            DeliveryResult::Duplicate { accepted_digest }
1012        }
1013        other => other,
1014    })
1015}