1use 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
53const CONTROL_BYTES: usize = 1024 * 1024;
55const MAX_COUNTERS: usize = 65_536;
59
60#[derive(Debug, Clone)]
62pub struct ContextClientConfig {
63 pub source_id: String,
65 pub in_flight: u32,
67 pub queue_capacity: u32,
68 pub transit_margin_ms: u64,
70 pub describe_budget_ms: u64,
72 pub mapping_version: String,
75 pub principal_scope: String,
76}
77
78pub 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#[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
114fn escape(component: &str) -> String {
116 component
117 .replace('%', "%25")
118 .replace('/', "%2F")
119 .replace(',', "%2C")
120}
121
122impl RemoteContextClient {
123 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 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 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 pub fn counters(&self) -> SeamCounters {
192 self.inner.queue.counters()
193 }
194
195 pub fn exchange_failures(&self) -> u64 {
197 self.inner.exchange_failures.load(Ordering::Relaxed)
198 }
199
200 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 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
430fn 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
450struct 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#[derive(Clone)]
488pub struct RemoteAcquisitionSource {
489 client: RemoteContextClient,
490 descriptor: Arc<SourceDescriptor>,
491}
492
493impl RemoteContextClient {
494 pub fn acquisition_source(&self) -> Option<RemoteAcquisitionSource> {
496 let hosted = self.inner.source.descriptor.as_ref()?;
497 let descriptor = hosted.document.document().ok()?;
499 Some(RemoteAcquisitionSource {
500 client: self.clone(),
501 descriptor: Arc::new(descriptor),
502 })
503 }
504}
505
506fn 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
530struct 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 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
564fn 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 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 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 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
785fn 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 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 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 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}