Skip to main content

rustev_remote_http/
context_client.rs

1//! The `rustev.remote-context/1` HTTP client (spec 042, 3.10 and 3.12): a
2//! `ContextSource` backed by a context source in another process.
3//!
4//! It describes the endpoint once, at construction, and never per decision;
5//! an unknown version, a missing source or another endpoint class is a
6//! capability failure there. Each fetch goes through the client's bounded
7//! [`SeamQueue`]: a full queue fails at once with `remote_seam_queue_full`,
8//! nothing is enqueued without bound, and a fetch carries the remaining
9//! budget less the declared transit margin. A fetch is sent at most once per
10//! attempt id `<decision id>/context/<n>`, with no invisible retry. When the
11//! caller stops waiting, the exchange is cancelled at the endpoint (when it
12//! offers cancellation) and an answer that still arrives is discarded,
13//! counted and recorded as late; it never reaches a snapshot. Each exchange
14//! yields a digest-only `rustev.remote-context-exchange/1` record to the
15//! host's exchange sink, whose failures are counted and change nothing.
16//!
17//! When describe carries a source's spec 036 descriptor, the client also
18//! yields a [`RemoteAcquisitionSource`]: an ordinary installed
19//! `AcquisitionSource` sending `acquire` through the same queue (3.10.3).
20
21use std::collections::HashMap;
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::sync::{Arc, Mutex};
24
25use rustev_contract::Document;
26use rustev_contract::acquisition::{AcquisitionRequest, OperationCost};
27use rustev_contract::ids::ContentDigest;
28use rustev_contract::remote::{CancelSupport, MAX_TEXT_BYTES, RemoteCode, WirePrincipal, cut_to};
29use rustev_contract::remote_context::{
30    self as rc, AcquireCode, AcquireResult, CancelItem, FetchResult, HostedSource,
31    InvalidationsResponse,
32};
33use rustev_contract::run::{Charge, CostBound};
34use rustev_contract::serve::Embedded;
35use rustev_contract::snapshot::Snapshot;
36use rustev_contract::source::SourceDescriptor;
37use rustev_core::seams::{
38    AcquireCall, AcquireReport, AcquisitionFailure, AcquisitionSource, AdapterFailure, BoxFuture,
39    CallContext, CancelAck, ContextSource,
40};
41use tokio::sync::oneshot;
42
43use crate::budget::Budget;
44use crate::callback_server::{
45    CONTEXT_ACQUIRE_PATH, CONTEXT_CANCEL_PATH, CONTEXT_DESCRIBE_PATH, CONTEXT_FETCH_PATH,
46    CONTEXT_INVALIDATIONS_PATH,
47};
48use crate::http::{HttpTransport, TransportError};
49use crate::seam_queue::{QUEUE_FULL, SeamCounters, SeamQueue};
50use crate::taxonomy::{classify_status, failure_detail, is_local_only};
51use crate::wire::SentTracker;
52
53/// Describe answers and control documents are small.
54const CONTROL_BYTES: usize = 1024 * 1024;
55/// Attempt counters remembered at most; beyond it they restart, and a
56/// reused attempt id with other content is refused by the endpoint
57/// (`attempt_id_conflict`), never answered with another fetch's snapshot.
58const MAX_COUNTERS: usize = 65_536;
59
60/// How one client talks to one source.
61#[derive(Debug, Clone)]
62pub struct ContextClientConfig {
63    /// The source id the endpoint describes.
64    pub source_id: String,
65    /// In-flight limit and queue capacity, each 1 to 65,536 (3.12.1).
66    pub in_flight: u32,
67    pub queue_capacity: u32,
68    /// Subtracted from the remaining budget before it is sent (3.10.2).
69    pub transit_margin_ms: u64,
70    /// The budget of the describe exchange at construction.
71    pub describe_budget_ms: u64,
72    /// The principal reference's mapping version and declared scope (spec
73    /// 020, 3.6); the reference is the call's principal handle.
74    pub mapping_version: String,
75    pub principal_scope: String,
76}
77
78/// Receives each exchange record; an error is counted, never propagated.
79pub type ExchangeSink = Arc<dyn Fn(&rc::ExchangeRecord) -> Result<(), String> + Send + Sync>;
80
81struct Inner {
82    transport: HttpTransport,
83    config: ContextClientConfig,
84    source: HostedSource,
85    describe_digest: ContentDigest,
86    queue: SeamQueue,
87    counters: Mutex<HashMap<String, u32>>,
88    exchanges: Option<ExchangeSink>,
89    exchange_failures: AtomicU64,
90}
91
92/// The client. Clones share the queue and counters.
93#[derive(Clone)]
94pub struct RemoteContextClient {
95    inner: Arc<Inner>,
96}
97
98fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
99    m.lock().unwrap_or_else(|e| e.into_inner())
100}
101
102fn transport_code(error: &TransportError) -> RemoteCode {
103    match error {
104        TransportError::Unsent(_) => RemoteCode::TransportUnsent,
105        TransportError::Interrupted(_) => RemoteCode::TransportInterrupted,
106        TransportError::TooLarge { .. } => RemoteCode::MalformedResponse,
107        TransportError::Expired { sent: false } => RemoteCode::TransportUnsent,
108        TransportError::Expired { sent: true } => RemoteCode::RemoteDeadline,
109        TransportError::PhaseExpired(p) if p.bytes_sent => RemoteCode::RemoteDeadline,
110        TransportError::PhaseExpired(_) => RemoteCode::TransportUnsent,
111    }
112}
113
114/// Spec 003 3.9.2's escaping of an attempt id component.
115fn escape(component: &str) -> String {
116    component
117        .replace('%', "%25")
118        .replace('/', "%2F")
119        .replace(',', "%2C")
120}
121
122impl RemoteContextClient {
123    /// Describe the endpoint and select `config.source_id`.
124    pub async fn connect(
125        transport: HttpTransport,
126        config: ContextClientConfig,
127        exchanges: Option<ExchangeSink>,
128    ) -> Result<Self, String> {
129        let queue = SeamQueue::new(config.in_flight, config.queue_capacity)?;
130        let request = rc::DescribeRequest {
131            schema: rc::DESCRIBE.into(),
132            versions: vec![1],
133        }
134        .record_canonical()
135        .map_err(|e| e.to_string())?;
136        let reply = transport
137            .post(
138                CONTEXT_DESCRIBE_PATH,
139                request,
140                CONTROL_BYTES,
141                &Budget::from_ms(config.describe_budget_ms),
142                &SentTracker::new(),
143            )
144            .await
145            .map_err(|e| failure_detail(transport_code(&e), "describe"))?;
146        if let Some(code) = classify_status(reply.status) {
147            return Err(failure_detail(code, "describe"));
148        }
149        // An endpoint class other than host-operated does not parse.
150        let described = rc::DescribeResponse::parse(&reply.body)
151            .map_err(|e| failure_detail(RemoteCode::Capability, &e.to_string()))?;
152        if described.version != 1 {
153            return Err(failure_detail(RemoteCode::Capability, "unknown version"));
154        }
155        let Some(source) = described
156            .sources
157            .iter()
158            .find(|s| s.source_id == config.source_id)
159            .cloned()
160        else {
161            return Err(failure_detail(
162                RemoteCode::Capability,
163                "source not described",
164            ));
165        };
166        let describe_digest = described.record_digest().map_err(|e| e.to_string())?;
167        Ok(RemoteContextClient {
168            inner: Arc::new(Inner {
169                transport,
170                config,
171                source,
172                describe_digest,
173                queue,
174                counters: Mutex::new(HashMap::new()),
175                exchanges,
176                exchange_failures: AtomicU64::new(0),
177            }),
178        })
179    }
180
181    /// What the endpoint described for this source.
182    pub fn source(&self) -> &HostedSource {
183        &self.inner.source
184    }
185
186    pub fn describe_digest(&self) -> &ContentDigest {
187        &self.inner.describe_digest
188    }
189
190    /// Queue depth, refusals and late discards (3.12.4).
191    pub fn counters(&self) -> SeamCounters {
192        self.inner.queue.counters()
193    }
194
195    /// Exchange records the host's exchange sink failed to take.
196    pub fn exchange_failures(&self) -> u64 {
197        self.inner.exchange_failures.load(Ordering::Relaxed)
198    }
199
200    /// Ask the endpoint to cancel attempts (3.10.4).
201    pub async fn cancel(
202        &self,
203        attempt_ids: Vec<String>,
204        budget_ms: u64,
205    ) -> Result<Vec<CancelItem>, String> {
206        let body = rc::CancelRequest {
207            schema: rc::CANCEL.into(),
208            attempt_ids,
209        }
210        .record_canonical()
211        .map_err(|e| e.to_string())?;
212        let reply = self
213            .inner
214            .transport
215            .post(
216                CONTEXT_CANCEL_PATH,
217                body,
218                CONTROL_BYTES,
219                &Budget::from_ms(budget_ms),
220                &SentTracker::new(),
221            )
222            .await
223            .map_err(|e| failure_detail(transport_code(&e), "cancel"))?;
224        if let Some(code) = classify_status(reply.status) {
225            return Err(failure_detail(code, "cancel"));
226        }
227        let answers = rc::CancelResponse::parse(&reply.body)
228            .map_err(|e| failure_detail(RemoteCode::MalformedResponse, &e.to_string()))?
229            .answers;
230        if self.inner.source.terms.cancellation != CancelSupport::Confirmed
231            && answers
232                .iter()
233                .any(|a| a.answer == rc::CancelAnswer::Stopped)
234        {
235            return Err(failure_detail(
236                RemoteCode::MalformedResponse,
237                "stopped without confirmed cancellation",
238            ));
239        }
240        Ok(answers)
241    }
242
243    /// Poll invalidations after `sequence`, or the baseline with `None`
244    /// (3.10.6). Applying them is the caller's.
245    pub async fn invalidations(
246        &self,
247        sequence: Option<u64>,
248        budget_ms: u64,
249    ) -> Result<InvalidationsResponse, String> {
250        let body = rc::InvalidationsRequest {
251            schema: rc::INVALIDATIONS.into(),
252            sequence,
253        }
254        .record_canonical()
255        .map_err(|e| e.to_string())?;
256        let reply = self
257            .inner
258            .transport
259            .post(
260                CONTEXT_INVALIDATIONS_PATH,
261                body,
262                CONTROL_BYTES,
263                &Budget::from_ms(budget_ms),
264                &SentTracker::new(),
265            )
266            .await
267            .map_err(|e| failure_detail(transport_code(&e), "invalidations"))?;
268        if let Some(code) = classify_status(reply.status) {
269            return Err(failure_detail(code, "invalidations"));
270        }
271        let answer = InvalidationsResponse::parse(&reply.body)
272            .map_err(|e| failure_detail(RemoteCode::MalformedResponse, &e.to_string()))?;
273        let mut last = sequence;
274        for item in &answer.items {
275            if last.is_some_and(|l| item.sequence <= l) {
276                return Err(failure_detail(
277                    RemoteCode::MalformedResponse,
278                    "sequence not increasing",
279                ));
280            }
281            last = Some(item.sequence);
282        }
283        if sequence.is_some_and(|s| answer.sequence < s)
284            || last.is_some_and(|l| answer.sequence < l)
285        {
286            return Err(failure_detail(
287                RemoteCode::MalformedResponse,
288                "sequence went back",
289            ));
290        }
291        Ok(answer)
292    }
293
294    fn next_attempt(&self, decision: &str) -> String {
295        let mut counters = lock(&self.inner.counters);
296        if counters.len() >= MAX_COUNTERS && !counters.contains_key(decision) {
297            counters.clear();
298        }
299        let n = counters.entry(decision.to_string()).or_insert(0);
300        *n += 1;
301        format!("{}/context/{}", escape(decision), n)
302    }
303
304    fn record(
305        &self,
306        attempt_id: &str,
307        request: Option<ContentDigest>,
308        response: Option<ContentDigest>,
309        code: Option<AcquireCode>,
310        late: bool,
311    ) {
312        let Some(sink) = &self.inner.exchanges else {
313            return;
314        };
315        let record = rc::ExchangeRecord {
316            schema: rc::EXCHANGE.into(),
317            attempt_id: attempt_id.into(),
318            request_digest: request,
319            response_digest: response,
320            code,
321            phases: vec![],
322            late,
323        };
324        if sink(&record).is_err() {
325            self.inner.exchange_failures.fetch_add(1, Ordering::Relaxed);
326        }
327    }
328
329    async fn fetch_once(&self, fields: &[String], cx: &CallContext) -> Result<Snapshot, String> {
330        let inner = &self.inner;
331        let slot = inner
332            .queue
333            .try_enter()
334            .map_err(|_| QUEUE_FULL.to_string())?;
335        let Some(principal) = WirePrincipal::encode(
336            &cx.principal_handle,
337            &inner.config.mapping_version,
338            &inner.config.principal_scope,
339        ) else {
340            return Err(failure_detail(
341                RemoteCode::MalformedRequest,
342                "no principal reference",
343            ));
344        };
345        let budget = Budget::start(cx.remaining_ms);
346        let told = budget.told_ms(inner.config.transit_margin_ms);
347        if told == 0 {
348            return Err(failure_detail(
349                RemoteCode::RemoteDeadline,
350                "budget spent before sending",
351            ));
352        }
353        let attempt_id = self.next_attempt(&cx.trace_id);
354        let request = rc::FetchRequest {
355            schema: rc::FETCH.into(),
356            version: 1,
357            source_id: inner.config.source_id.clone(),
358            describe_digest: inner.describe_digest.clone(),
359            attempt_id: attempt_id.clone(),
360            budget_ms: told,
361            trace_id: cx.trace_id.clone(),
362            traceparent: None,
363            principal,
364            fields: fields.to_vec(),
365        };
366        let request_digest = request.record_digest().ok();
367        let body = request.record_canonical().map_err(|e| e.to_string())?;
368        if body.len() as u64 > inner.source.terms.max_request_bytes {
369            return Err(failure_detail(
370                RemoteCode::MalformedRequest,
371                "request exceeds the source's bound",
372            ));
373        }
374        let (tx, rx) = oneshot::channel();
375        let client = self.clone();
376        let wanted = fields.to_vec();
377        let id = attempt_id.clone();
378        tokio::spawn(async move {
379            let inner = &client.inner;
380            let Ok(_running) = slot.start(budget.deadline()).await else {
381                let _ = tx.send(Err(failure_detail(
382                    RemoteCode::TransportUnsent,
383                    "queue_deadline",
384                )));
385                return;
386            };
387            let max = inner.source.terms.max_response_bytes.min(usize::MAX as u64) as usize;
388            let reply = inner
389                .transport
390                .post(CONTEXT_FETCH_PATH, body, max, &budget, &SentTracker::new())
391                .await;
392            let (result, response_digest, answered) = match reply {
393                Err(e) => (Err(failure_detail(transport_code(&e), "")), None, false),
394                Ok(r) => match classify_status(r.status) {
395                    Some(code) => (Err(failure_detail(code, "")), None, true),
396                    None => {
397                        let (result, digest) = judge(&r.body, &wanted);
398                        (result, digest, true)
399                    }
400                },
401            };
402            let code = result
403                .as_ref()
404                .err()
405                .map(|d| crate::taxonomy::code_of_detail(d).unwrap_or(RemoteCode::RemoteError));
406            let late = match tx.send(result) {
407                Ok(()) => false,
408                Err(_) if answered => {
409                    inner.queue.count_late();
410                    true
411                }
412                Err(_) => false,
413            };
414            let code = code.map(AcquireCode::from);
415            client.record(&id, request_digest, response_digest, code, late);
416        });
417        let mut guard = Abandon {
418            client: self.clone(),
419            attempt_id,
420            armed: true,
421        };
422        let result = rx
423            .await
424            .unwrap_or_else(|_| Err(failure_detail(RemoteCode::RemoteError, "exchange ended")));
425        guard.armed = false;
426        result
427    }
428}
429
430/// The answer's snapshot, or the failure it names.
431fn judge(body: &[u8], wanted: &[String]) -> (Result<Snapshot, String>, Option<ContentDigest>) {
432    let malformed = |why: &str| Err(failure_detail(RemoteCode::MalformedResponse, why));
433    let Ok(answer) = rc::FetchResponse::parse(body) else {
434        return (malformed("unparsable answer"), None);
435    };
436    let digest = answer.record_digest().ok();
437    let result = match answer.result {
438        FetchResult::Failure { code, .. } if is_local_only(code.0) => {
439            malformed("a local-only code from the endpoint")
440        }
441        FetchResult::Failure { code, detail } => Err(failure_detail(code.0, &detail)),
442        FetchResult::Snapshot(s) if s.entries.iter().any(|e| !wanted.contains(&e.field)) => {
443            malformed("an unrequested field")
444        }
445        FetchResult::Snapshot(s) => Ok(s),
446    };
447    (result, digest)
448}
449
450/// Cancels at the endpoint when the caller stops waiting.
451struct Abandon {
452    client: RemoteContextClient,
453    attempt_id: String,
454    armed: bool,
455}
456
457impl Drop for Abandon {
458    fn drop(&mut self) {
459        if !self.armed || self.client.inner.source.terms.cancellation == CancelSupport::None {
460            return;
461        }
462        let client = self.client.clone();
463        let id = std::mem::take(&mut self.attempt_id);
464        if let Ok(rt) = tokio::runtime::Handle::try_current() {
465            rt.spawn(async move {
466                let _ = client.cancel(vec![id], 1_000).await;
467            });
468        }
469    }
470}
471
472impl ContextSource for RemoteContextClient {
473    fn fetch<'a>(
474        &'a self,
475        fields: &'a [String],
476        cx: &'a CallContext,
477    ) -> BoxFuture<'a, Result<Snapshot, String>> {
478        Box::pin(self.fetch_once(fields, cx))
479    }
480}
481
482/// One described source's acquisitions (3.10.3): an ordinary installed
483/// `AcquisitionSource` whose descriptor is exactly the one describe
484/// returned. It validates nothing spec 036 3.3.4 validates: a page's bytes
485/// go to the runtime as the endpoint sent them. Clones share the client's
486/// queue, so fetches and acquisitions share its in-flight limit (3.12.1).
487#[derive(Clone)]
488pub struct RemoteAcquisitionSource {
489    client: RemoteContextClient,
490    descriptor: Arc<SourceDescriptor>,
491}
492
493impl RemoteContextClient {
494    /// The source's acquisitions, when describe carried its descriptor.
495    pub fn acquisition_source(&self) -> Option<RemoteAcquisitionSource> {
496        let hosted = self.inner.source.descriptor.as_ref()?;
497        // Describe parsing already checked the document and its identity.
498        let descriptor = hosted.document.document().ok()?;
499        Some(RemoteAcquisitionSource {
500            client: self.clone(),
501            descriptor: Arc::new(descriptor),
502        })
503    }
504}
505
506/// The failure class an acquire code maps to (3.10.3): `unauthorized` and
507/// `denied` are `denied`, never retried; the others as spec 009 3.6 maps
508/// them (a remote `cancelled` the caller did not ask for is transient).
509fn acquire_class(code: AcquireCode) -> AcquisitionFailure {
510    match code {
511        AcquireCode::Denied | AcquireCode::Context(rc::ContextCode(RemoteCode::Unauthorized)) => {
512            AcquisitionFailure::Denied
513        }
514        AcquireCode::Context(rc::ContextCode(code)) => match crate::taxonomy::failure_class(code) {
515            AdapterFailure::Overloaded => AcquisitionFailure::Overloaded,
516            AdapterFailure::Permanent => AcquisitionFailure::Permanent,
517            _ => AcquisitionFailure::Transient,
518        },
519    }
520}
521
522fn acquire_detail(code: AcquireCode, detail: &str) -> String {
523    match code {
524        AcquireCode::Context(rc::ContextCode(code)) => failure_detail(code, detail),
525        AcquireCode::Denied if detail.is_empty() => "remote:denied".into(),
526        AcquireCode::Denied => cut_to(&format!("remote:denied: {detail}"), MAX_TEXT_BYTES),
527    }
528}
529
530/// A failed attempt, before it becomes a report.
531struct Failed {
532    code: AcquireCode,
533    detail: String,
534    charge: Charge,
535}
536
537impl Failed {
538    fn local(code: RemoteCode, detail: &str, sent: bool) -> Self {
539        Failed {
540            code: code.into(),
541            detail: detail.into(),
542            // Nothing that never left the process was charged.
543            charge: if sent {
544                crate::taxonomy::charge_for(code, None, true)
545            } else {
546                Charge::Observed { units: 0 }
547            },
548        }
549    }
550    fn report(self) -> AcquireReport {
551        AcquireReport {
552            result: Err((
553                acquire_class(self.code),
554                acquire_detail(self.code, &self.detail),
555            )),
556            charge: self.charge,
557            cancel: CancelAck::NotRequested,
558        }
559    }
560}
561
562type Answer = Result<(Vec<u8>, Charge), Failed>;
563
564/// The answer's page bytes and charge, or the failure it names.
565fn judge_acquire(body: &[u8]) -> (Answer, Option<ContentDigest>) {
566    let Ok(answer) = rc::AcquireResponse::parse(body) else {
567        return (
568            Err(Failed::local(
569                RemoteCode::MalformedResponse,
570                "unparsable answer",
571                true,
572            )),
573            None,
574        );
575    };
576    let digest = answer.record_digest().ok();
577    let result = match answer.result {
578        AcquireResult::Failure {
579            code: AcquireCode::Context(rc::ContextCode(code)),
580            ..
581        } if is_local_only(code) => Err(Failed::local(
582            RemoteCode::MalformedResponse,
583            "a local-only code from the endpoint",
584            true,
585        )),
586        AcquireResult::Failure { code, detail } => Err(Failed {
587            code,
588            detail,
589            charge: answer.charge,
590        }),
591        AcquireResult::Page(page) => Ok((page.bytes().to_vec(), answer.charge)),
592    };
593    (result, digest)
594}
595
596impl RemoteAcquisitionSource {
597    pub fn descriptor_id(&self) -> &rustev_contract::ids::SourceId {
598        &self
599            .client
600            .inner
601            .source
602            .descriptor
603            .as_ref()
604            .expect("an acquisition source has a descriptor")
605            .id
606    }
607
608    /// The shared client queue's depth, refusals and late discards.
609    pub fn counters(&self) -> SeamCounters {
610        self.client.counters()
611    }
612
613    async fn acquire_once(&self, call: AcquireCall<'_>) -> AcquireReport {
614        let inner = &self.client.inner;
615        let Ok(slot) = inner.queue.try_enter() else {
616            // Spec 036's unavailable outcome with the queue's reason (3.12.1).
617            return AcquireReport {
618                result: Err((AcquisitionFailure::Overloaded, QUEUE_FULL.into())),
619                charge: Charge::Observed { units: 0 },
620                cancel: CancelAck::NotRequested,
621            };
622        };
623        let Some(principal) = WirePrincipal::encode(
624            &call.cx.principal_handle,
625            &inner.config.mapping_version,
626            &inner.config.principal_scope,
627        ) else {
628            return Failed::local(
629                RemoteCode::MalformedRequest,
630                "no principal reference",
631                false,
632            )
633            .report();
634        };
635        let budget = Budget::start(call.cx.remaining_ms);
636        let told = budget.told_ms(inner.config.transit_margin_ms);
637        if told == 0 {
638            return Failed::local(
639                RemoteCode::RemoteDeadline,
640                "budget spent before sending",
641                false,
642            )
643            .report();
644        }
645        let request = match call
646            .request
647            .canonical()
648            .map_err(|e| e.to_string())
649            .and_then(|b| Embedded::<AcquisitionRequest>::parse(&b).map_err(|e| e.to_string()))
650        {
651            Ok(r) => r,
652            Err(e) => return Failed::local(RemoteCode::MalformedRequest, &e, false).report(),
653        };
654        let request = rc::AcquireRequest {
655            schema: rc::ACQUIRE.into(),
656            version: 1,
657            source: call.request.source.clone(),
658            operation: call.operation.into(),
659            request,
660            request_id: call.request_id.clone(),
661            attempt_id: call.attempt_id.into(),
662            budget_ms: told,
663            trace_id: call.cx.trace_id.clone(),
664            traceparent: None,
665            principal,
666        };
667        let request_digest = request.record_digest().ok();
668        let body = match request.record_canonical() {
669            Ok(b) if b.len() as u64 <= inner.source.terms.max_request_bytes => b,
670            Ok(_) => {
671                return Failed::local(
672                    RemoteCode::MalformedRequest,
673                    "request exceeds the source's bound",
674                    false,
675                )
676                .report();
677            }
678            Err(e) => {
679                return Failed::local(RemoteCode::MalformedRequest, &e.to_string(), false).report();
680            }
681        };
682        let (tx, rx) = oneshot::channel::<Answer>();
683        let client = self.client.clone();
684        let id = call.attempt_id.to_string();
685        let sent = SentTracker::new();
686        let tracker = sent.clone();
687        tokio::spawn(async move {
688            let inner = &client.inner;
689            let Ok(_running) = slot.start(budget.deadline()).await else {
690                let _ = tx.send(Err(Failed::local(
691                    RemoteCode::TransportUnsent,
692                    "queue_deadline",
693                    false,
694                )));
695                return;
696            };
697            let max = inner.source.terms.max_response_bytes.min(usize::MAX as u64) as usize;
698            let reply = inner
699                .transport
700                .post(CONTEXT_ACQUIRE_PATH, body, max, &budget, &tracker)
701                .await;
702            let (result, response_digest, answered) = match reply {
703                Err(e) => {
704                    let code = transport_code(&e);
705                    (
706                        Err(Failed::local(code, "", tracker.may_have_sent())),
707                        None,
708                        false,
709                    )
710                }
711                Ok(r) => match classify_status(r.status) {
712                    Some(code) => (Err(Failed::local(code, "", true)), None, true),
713                    None => {
714                        let (result, digest) = judge_acquire(&r.body);
715                        (result, digest, true)
716                    }
717                },
718            };
719            let code = result.as_ref().err().map(|f| f.code);
720            let late = match tx.send(result) {
721                Ok(()) => false,
722                Err(_) if answered => {
723                    inner.queue.count_late();
724                    true
725                }
726                Err(_) => false,
727            };
728            client.record(&id, request_digest, response_digest, code, late);
729        });
730        let mut guard = Abandon {
731            client: self.client.clone(),
732            attempt_id: call.attempt_id.to_string(),
733            armed: true,
734        };
735        let answer = tokio::select! {
736            answer = rx => answer.unwrap_or_else(|_| {
737                Err(Failed::local(RemoteCode::RemoteError, "exchange ended", true))
738            }),
739            () = call.cancel.raised() => {
740                guard.armed = false;
741                return self.cancelled(&sent, call.attempt_id).await;
742            }
743        };
744        guard.armed = false;
745        match answer {
746            Ok((page, charge)) => AcquireReport {
747                result: Ok(page),
748                charge,
749                cancel: CancelAck::NotRequested,
750            },
751            Err(failed) => failed.report(),
752        }
753    }
754
755    /// The caller cancelled: nothing was sent, or the endpoint is asked to
756    /// stop and only a confirmed `stopped` is reported as one (3.10.4).
757    async fn cancelled(&self, sent: &SentTracker, attempt_id: &str) -> AcquireReport {
758        let report = |cancel, charge| AcquireReport {
759            result: Err((
760                AcquisitionFailure::Transient,
761                failure_detail(RemoteCode::Cancelled, ""),
762            )),
763            charge,
764            cancel,
765        };
766        if sent.try_abort() {
767            return report(CancelAck::Stopped, Charge::Observed { units: 0 });
768        }
769        if self.client.inner.source.terms.cancellation == CancelSupport::None {
770            return report(CancelAck::Unconfirmed, Charge::Unknown);
771        }
772        let support = self.client.inner.source.terms.cancellation;
773        let ack = match self
774            .client
775            .cancel(vec![attempt_id.to_string()], 1_000)
776            .await
777        {
778            Ok(answers) => remote_cancel_ack(support, &answers, attempt_id),
779            Err(_) => CancelAck::Unconfirmed,
780        };
781        report(ack, Charge::Unknown)
782    }
783}
784
785/// The acknowledgement of a sent attempt's cancel: `stopped` only when the
786/// endpoint declared `confirmed` cancellation and answered `stopped` for
787/// this attempt; `best_effort` never yields `stopped` (3.10.4).
788fn remote_cancel_ack(
789    support: CancelSupport,
790    answers: &[CancelItem],
791    attempt_id: &str,
792) -> CancelAck {
793    let stopped = answers
794        .iter()
795        .any(|a| a.attempt_id == attempt_id && a.answer == rc::CancelAnswer::Stopped);
796    if support == CancelSupport::Confirmed && stopped {
797        CancelAck::Stopped
798    } else {
799        CancelAck::Unconfirmed
800    }
801}
802
803impl AcquisitionSource for RemoteAcquisitionSource {
804    fn descriptor(&self) -> &SourceDescriptor {
805        &self.descriptor
806    }
807
808    /// The operation's declared bound; an estimated or unknown cost model
809    /// states no units, so it is `unknown`.
810    fn cost_bound(&self, request: &AcquisitionRequest) -> CostBound {
811        let declared = self
812            .descriptor
813            .operations
814            .iter()
815            .flatten()
816            .find(|o| o.name == request.operation)
817            .map(|o| &o.cost);
818        match declared {
819            Some(OperationCost::Bounded { max_units }) => CostBound::Bounded {
820                max_units: *max_units,
821            },
822            _ => CostBound::Unknown,
823        }
824    }
825
826    fn acquire<'a>(&'a self, call: AcquireCall<'a>) -> BoxFuture<'a, AcquireReport> {
827        Box::pin(self.acquire_once(call))
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834
835    fn answer(id: &str, answer: rc::CancelAnswer) -> CancelItem {
836        CancelItem {
837            attempt_id: id.into(),
838            answer,
839        }
840    }
841
842    #[test]
843    fn stopped_needs_confirmed_support_and_this_attempts_stop() {
844        let stopped = [answer("a", rc::CancelAnswer::Stopped)];
845        assert_eq!(
846            remote_cancel_ack(CancelSupport::Confirmed, &stopped, "a"),
847            CancelAck::Stopped
848        );
849        // Best effort never yields stopped, even when the endpoint says so.
850        assert_eq!(
851            remote_cancel_ack(CancelSupport::BestEffort, &stopped, "a"),
852            CancelAck::Unconfirmed
853        );
854        assert_eq!(
855            remote_cancel_ack(CancelSupport::None, &stopped, "a"),
856            CancelAck::Unconfirmed
857        );
858        // Another attempt's stop says nothing about this one.
859        assert_eq!(
860            remote_cancel_ack(CancelSupport::Confirmed, &stopped, "b"),
861            CancelAck::Unconfirmed
862        );
863        let unconfirmed = [answer("a", rc::CancelAnswer::Unconfirmed)];
864        assert_eq!(
865            remote_cancel_ack(CancelSupport::Confirmed, &unconfirmed, "a"),
866            CancelAck::Unconfirmed
867        );
868    }
869}