Skip to main content

rustev_remote_http/
cancel.rs

1//! Cancellation that never claims more than is known (spec 009, 3.4.3 and
2//! I-6): `stopped` only when no request byte left the process, or when the
3//! remote side confirmed the stop with its final charge; `best_effort`
4//! never yields `stopped`.
5
6use rustev_contract::remote::{CancelSupport, RemoteCode};
7use rustev_contract::run::Charge;
8use rustev_core::seams::{AdapterFailure, AttemptReport, CancelAck, RemoteEnd};
9
10use crate::taxonomy::failure_detail;
11
12/// What a cancelled attempt can state.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct CancelOutcome {
15    pub ack: CancelAck,
16    pub charge: Charge,
17    pub remote: RemoteEnd,
18}
19
20/// Decide the acknowledgement of a cancelled attempt.
21///
22/// - `sent == false` (no request byte can ever leave; see
23///   [`crate::wire::SentTracker::try_abort`]): `stopped`, `observed{0}`.
24/// - sent, `support == confirmed` and the remote side confirmed the stop
25///   with `confirmed_charge` within the budget: `stopped` with that charge.
26/// - otherwise: `unconfirmed`, charge `unknown`, possibly continuing.
27pub fn cancel_outcome(
28    sent: bool,
29    support: CancelSupport,
30    confirmed_charge: Option<Charge>,
31) -> CancelOutcome {
32    if !sent {
33        return CancelOutcome {
34            ack: CancelAck::Stopped,
35            charge: Charge::Observed { units: 0 },
36            remote: RemoteEnd::Finished,
37        };
38    }
39    match (support, confirmed_charge) {
40        (CancelSupport::Confirmed, Some(c)) if c != Charge::Unknown => CancelOutcome {
41            ack: CancelAck::Stopped,
42            charge: c,
43            remote: RemoteEnd::Finished,
44        },
45        _ => CancelOutcome {
46            ack: CancelAck::Unconfirmed,
47            charge: Charge::Unknown,
48            remote: RemoteEnd::PossiblyContinuing,
49        },
50    }
51}
52
53/// The report of a cancelled attempt, `remote:cancelled` with the outcome of
54/// [`cancel_outcome`].
55pub fn cancelled_report(outcome: CancelOutcome, detail: &str) -> AttemptReport {
56    AttemptReport {
57        result: Err((
58            AdapterFailure::Cancelled,
59            failure_detail(RemoteCode::Cancelled, detail),
60        )),
61        charge: outcome.charge,
62        cancel: outcome.ack,
63        remote: outcome.remote,
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn stopped_only_when_unsent_or_confirmed() {
73        for support in [
74            CancelSupport::None,
75            CancelSupport::BestEffort,
76            CancelSupport::Confirmed,
77        ] {
78            let o = cancel_outcome(false, support, None);
79            assert_eq!(
80                (o.ack, o.charge),
81                (CancelAck::Stopped, Charge::Observed { units: 0 })
82            );
83            let o = cancel_outcome(true, support, None);
84            assert_eq!((o.ack, o.charge), (CancelAck::Unconfirmed, Charge::Unknown));
85            assert_eq!(o.remote, RemoteEnd::PossiblyContinuing);
86        }
87        let c = Some(Charge::Observed { units: 2 });
88        assert_eq!(
89            cancel_outcome(true, CancelSupport::Confirmed, c).ack,
90            CancelAck::Stopped
91        );
92        // Best effort never yields stopped, even with a charge in hand.
93        assert_eq!(
94            cancel_outcome(true, CancelSupport::BestEffort, c).ack,
95            CancelAck::Unconfirmed
96        );
97        // A confirmation without a final charge is not a confirmed stop.
98        assert_eq!(
99            cancel_outcome(true, CancelSupport::Confirmed, Some(Charge::Unknown)).ack,
100            CancelAck::Unconfirmed
101        );
102    }
103}