1use std::io;
8use std::time::Duration;
9
10use map_core::{MapError, ObexError};
11use tokio_retry::strategy::ExponentialBackoff;
12
13use crate::{SessionError, TransportError};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Disposition {
18 Transient,
20 Permanent,
22}
23
24#[must_use]
32pub fn classify(e: &SessionError) -> Disposition {
33 match e {
34 SessionError::Transport(t) => classify_transport(t),
35 SessionError::Map(m) => classify_map(m),
36 SessionError::Pbap(_) => Disposition::Transient,
37 }
38}
39
40fn classify_map(e: &MapError) -> Disposition {
41 match e {
42 MapError::Transport(t) => classify_transport(t),
43 MapError::Obex(ObexError::ConnectRejected(_))
45 | MapError::ServerError(_)
46 | MapError::InvalidInput(_) => Disposition::Permanent,
47 _ => Disposition::Transient,
48 }
49}
50
51fn classify_transport(e: &TransportError) -> Disposition {
52 match e {
53 TransportError::Io(io) => classify_io(io.kind()),
54 _ => Disposition::Transient,
55 }
56}
57
58const fn classify_io(kind: io::ErrorKind) -> Disposition {
59 match kind {
60 io::ErrorKind::ConnectionRefused
62 | io::ErrorKind::PermissionDenied
63 | io::ErrorKind::InvalidInput => Disposition::Permanent,
64 _ => Disposition::Transient,
65 }
66}
67
68pub fn backoff(
75 initial: Duration,
76 max: Duration,
77 max_attempts: u32,
78) -> impl Iterator<Item = Duration> + Clone {
79 let initial_ms = u64::try_from(initial.as_millis()).unwrap_or(u64::MAX);
80 let factor = (initial_ms / 2).max(1);
82 let gaps = usize::try_from(max_attempts.saturating_sub(1)).unwrap_or(usize::MAX);
83 ExponentialBackoff::from_millis(2).factor(factor).max_delay(max).take(gaps)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn io(kind: io::ErrorKind) -> SessionError {
91 SessionError::Transport(TransportError::Io(io::Error::new(kind, "x")))
92 }
93
94 #[test]
95 fn timeouts_and_resets_are_transient() {
96 for k in
97 [io::ErrorKind::TimedOut, io::ErrorKind::ConnectionReset, io::ErrorKind::BrokenPipe]
98 {
99 assert_eq!(classify(&io(k)), Disposition::Transient);
100 }
101 }
102
103 #[test]
104 fn refusal_and_auth_are_permanent() {
105 for k in [io::ErrorKind::ConnectionRefused, io::ErrorKind::PermissionDenied] {
106 assert_eq!(classify(&io(k)), Disposition::Permanent);
107 }
108 }
109
110 #[test]
111 fn obex_connect_rejected_is_permanent() {
112 let e = SessionError::Map(MapError::Obex(ObexError::ConnectRejected(0xC3)));
113 assert_eq!(classify(&e), Disposition::Permanent);
114 }
115
116 #[test]
117 fn server_error_is_permanent_but_eof_is_transient() {
118 assert_eq!(
119 classify(&SessionError::Map(MapError::ServerError(0xC0))),
120 Disposition::Permanent
121 );
122 assert_eq!(classify(&SessionError::Map(MapError::UnexpectedEof)), Disposition::Transient);
123 }
124
125 #[test]
126 fn backoff_doubles_from_initial_capped_and_bounded() {
127 let delays: Vec<_> =
128 backoff(Duration::from_millis(500), Duration::from_secs(2), 5).collect();
129 assert_eq!(
131 delays.as_slice(),
132 [
133 Duration::from_millis(500),
134 Duration::from_secs(1),
135 Duration::from_secs(2),
136 Duration::from_secs(2),
137 ]
138 );
139 }
140
141 #[test]
142 fn backoff_is_empty_for_single_attempt() {
143 assert_eq!(backoff(Duration::from_millis(10), Duration::from_secs(1), 1).count(), 0);
144 }
145}