1use std::task::{Poll, ready};
2use std::time::Duration;
3
4use moq_net::kio;
5use url::Url;
6
7use crate::{Client, Error};
8
9#[derive(Clone, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
11#[serde(default, deny_unknown_fields)]
12pub struct Backoff {
13 #[arg(
15 id = "backoff-initial",
16 long,
17 default_value = "1s",
18 env = "MOQ_BACKOFF_INITIAL",
19 value_parser = humantime::parse_duration,
20 )]
21 #[serde(with = "humantime_serde")]
22 pub initial: Duration,
23
24 #[arg(id = "backoff-multiplier", long, default_value_t = 2, env = "MOQ_BACKOFF_MULTIPLIER")]
26 pub multiplier: u32,
27
28 #[arg(
30 id = "backoff-max",
31 long,
32 default_value = "30s",
33 env = "MOQ_BACKOFF_MAX",
34 value_parser = humantime::parse_duration,
35 )]
36 #[serde(with = "humantime_serde")]
37 pub max: Duration,
38
39 #[arg(
44 id = "backoff-timeout",
45 long,
46 default_value = "5m",
47 env = "MOQ_BACKOFF_TIMEOUT",
48 value_parser = humantime::parse_duration,
49 )]
50 #[serde(with = "humantime_serde")]
51 pub timeout: Duration,
52}
53
54impl Default for Backoff {
55 fn default() -> Self {
56 Self {
57 initial: Duration::from_secs(1),
58 multiplier: 2,
59 max: Duration::from_secs(30),
60 timeout: Duration::from_secs(300),
61 }
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum Status {
68 Connected,
70 Disconnected,
72}
73
74#[derive(Default)]
79struct State {
80 status: Option<Status>,
82 error: Option<Error>,
84}
85
86pub struct Reconnect {
92 abort: tokio::task::AbortHandle,
93 state: kio::Consumer<State>,
94 last_reported: Option<Status>,
96}
97
98impl Reconnect {
99 pub(crate) fn new(client: Client, url: Url, backoff: Backoff) -> Self {
100 let producer = kio::Producer::<State>::default();
101 let state = producer.consume();
102 let task = tokio::spawn(async move {
103 if let Err(err) = Self::run(&producer, client, url, backoff).await {
104 tracing::error!(%err, "reconnect loop exited");
105 if let Ok(mut state) = producer.write() {
106 state.error = Some(err);
107 }
108 }
109 });
111 Self {
112 abort: task.abort_handle(),
113 state,
114 last_reported: None,
115 }
116 }
117
118 async fn run(state: &kio::Producer<State>, client: Client, url: Url, backoff: Backoff) -> crate::Result<()> {
119 let mut delay = backoff.initial;
120 let mut retry_start = tokio::time::Instant::now();
121 let mut last_error: Option<Error> = None;
122
123 loop {
124 if !backoff.timeout.is_zero() && retry_start.elapsed() > backoff.timeout {
125 let timeout = backoff.timeout;
126 let msg = match last_error {
127 Some(err) => format!("reconnect timed out after {timeout:?}: {err}"),
128 None => format!("reconnect timed out after {timeout:?}"),
129 };
130 return Err(Error::Reconnect(msg));
131 }
132
133 tracing::info!(%url, "connecting");
134
135 match client.connect(url.clone()).await {
136 Ok(session) => {
137 tracing::info!(%url, "connected");
138 if let Ok(mut state) = state.write() {
139 state.status = Some(Status::Connected);
140 }
141
142 let connected = tokio::time::Instant::now();
143 let closed = session.closed().await;
144 if let Ok(mut state) = state.write() {
145 state.status = Some(Status::Disconnected);
146 }
147
148 if connected.elapsed() >= backoff.initial {
149 tracing::warn!(%url, "session closed, reconnecting");
152 delay = backoff.initial;
153 retry_start = tokio::time::Instant::now();
154 last_error = None;
155 } else {
156 if let Err(err) = closed {
161 let err = Error::from(err);
162 tracing::warn!(%url, %err, "session severed immediately, retrying");
163 last_error = Some(err);
164 } else {
165 tracing::warn!(%url, "session severed immediately, retrying");
166 }
167 }
168 }
169 Err(err) => {
170 if err.is_auth() {
171 return Err(err);
172 }
173 last_error = Some(err);
174 }
175 }
176
177 tracing::warn!(%url, ?delay, "reconnecting after backoff");
178 tokio::time::sleep(delay).await;
179 delay = std::cmp::min(delay * backoff.multiplier, backoff.max);
180 }
181 }
182
183 pub fn poll_status(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Status>> {
188 let last = self.last_reported;
189 let status = match ready!(self.state.poll(waiter, |state| match state.status {
190 Some(status) if Some(status) != last => Poll::Ready(status),
191 _ => Poll::Pending,
192 })) {
193 Ok(status) => status,
194 Err(state) => return Poll::Ready(Err(terminal(&state))),
195 };
196
197 self.last_reported = Some(status);
198 Poll::Ready(Ok(status))
199 }
200
201 pub async fn status(&mut self) -> crate::Result<Status> {
207 kio::wait(|waiter| self.poll_status(waiter)).await
208 }
209
210 pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<crate::Result<()>> {
215 ready!(self.state.poll_closed(waiter));
216 Poll::Ready(match &self.state.read().error {
217 Some(err) => Err(err.clone()),
218 None => Ok(()),
219 })
220 }
221
222 pub async fn closed(&self) -> crate::Result<()> {
224 kio::wait(|waiter| self.poll_closed(waiter)).await
225 }
226}
227
228impl Drop for Reconnect {
229 fn drop(&mut self) {
230 self.abort.abort();
231 }
232}
233
234fn terminal(state: &State) -> Error {
236 match &state.error {
237 Some(err) => err.clone(),
238 None => Error::Reconnect("reconnect stopped".to_string()),
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_backoff_default() {
248 let backoff = Backoff::default();
249 assert_eq!(backoff.initial, Duration::from_secs(1));
250 assert_eq!(backoff.multiplier, 2);
251 assert_eq!(backoff.max, Duration::from_secs(30));
252 assert_eq!(backoff.timeout, Duration::from_secs(300));
253 }
254}