1use std::task::{Poll, ready};
2use std::time::Duration;
3
4use moq_net::kio;
5use moq_net::{BandwidthConsumer, BandwidthProducer, Version};
6use url::Url;
7
8use crate::{Client, Error};
9
10#[derive(Clone, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
12#[serde(default, deny_unknown_fields)]
13pub struct Backoff {
14 #[arg(
16 id = "backoff-initial",
17 long,
18 default_value = "1s",
19 env = "MOQ_BACKOFF_INITIAL",
20 value_parser = humantime::parse_duration,
21 )]
22 #[serde(with = "humantime_serde")]
23 pub initial: Duration,
24
25 #[arg(id = "backoff-multiplier", long, default_value_t = 2, env = "MOQ_BACKOFF_MULTIPLIER")]
27 pub multiplier: u32,
28
29 #[arg(
31 id = "backoff-max",
32 long,
33 default_value = "30s",
34 env = "MOQ_BACKOFF_MAX",
35 value_parser = humantime::parse_duration,
36 )]
37 #[serde(with = "humantime_serde")]
38 pub max: Duration,
39
40 #[arg(
45 id = "backoff-timeout",
46 long,
47 default_value = "5m",
48 env = "MOQ_BACKOFF_TIMEOUT",
49 value_parser = humantime::parse_duration,
50 )]
51 #[serde(with = "humantime_serde")]
52 pub timeout: Duration,
53}
54
55impl Default for Backoff {
56 fn default() -> Self {
57 Self {
58 initial: Duration::from_secs(1),
59 multiplier: 2,
60 max: Duration::from_secs(30),
61 timeout: Duration::from_secs(300),
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum Status {
69 Connected,
71 Disconnected,
73}
74
75#[derive(Default)]
80struct State {
81 status: Option<Status>,
83 version: Option<Version>,
85 error: Option<Error>,
87}
88
89pub struct Reconnect {
99 abort: tokio::task::AbortHandle,
100 state: kio::Consumer<State>,
101 send_bandwidth: BandwidthConsumer,
103 recv_bandwidth: BandwidthConsumer,
105 last_reported: Option<Status>,
107}
108
109impl Reconnect {
110 pub(crate) fn new(client: Client, url: Url, backoff: Backoff) -> Self {
111 let producer = kio::Producer::<State>::default();
112 let state = producer.consume();
113
114 let send_bw = BandwidthProducer::new();
117 let recv_bw = BandwidthProducer::new();
118 let send_bandwidth = send_bw.consume();
119 let recv_bandwidth = recv_bw.consume();
120
121 let task = tokio::spawn(async move {
122 if let Err(err) = Self::run(&producer, &send_bw, &recv_bw, client, url, backoff).await {
123 tracing::error!(%err, "reconnect loop exited");
124 if let Ok(mut state) = producer.write() {
125 state.error = Some(err);
126 }
127 }
128 });
130 Self {
131 abort: task.abort_handle(),
132 state,
133 send_bandwidth,
134 recv_bandwidth,
135 last_reported: None,
136 }
137 }
138
139 async fn run(
140 state: &kio::Producer<State>,
141 send_bw: &BandwidthProducer,
142 recv_bw: &BandwidthProducer,
143 client: Client,
144 url: Url,
145 backoff: Backoff,
146 ) -> crate::Result<()> {
147 let mut delay = backoff.initial;
148 let mut retry_start = tokio::time::Instant::now();
149 let mut last_error: Option<Error> = None;
150
151 loop {
152 if !backoff.timeout.is_zero() && retry_start.elapsed() > backoff.timeout {
153 let timeout = backoff.timeout;
154 let msg = match last_error {
155 Some(err) => format!("reconnect timed out after {timeout:?}: {err}"),
156 None => format!("reconnect timed out after {timeout:?}"),
157 };
158 return Err(Error::Reconnect(msg));
159 }
160
161 tracing::info!(%url, "connecting");
162
163 match client.connect(url.clone()).await {
164 Ok(session) => {
165 tracing::info!(%url, "connected");
166 if let Ok(mut state) = state.write() {
167 state.status = Some(Status::Connected);
168 state.version = Some(session.version());
169 }
170
171 let connected = tokio::time::Instant::now();
172 let closed = run_session(send_bw, recv_bw, &session).await;
175 if let Ok(mut state) = state.write() {
176 state.status = Some(Status::Disconnected);
177 state.version = None;
178 }
179 let _ = send_bw.set(None);
181 let _ = recv_bw.set(None);
182
183 if connected.elapsed() >= backoff.initial {
184 tracing::warn!(%url, "session closed, reconnecting");
187 delay = backoff.initial;
188 retry_start = tokio::time::Instant::now();
189 last_error = None;
190 } else {
191 if let Err(err) = closed {
196 let err = Error::from(err);
197 tracing::warn!(%url, %err, "session severed immediately, retrying");
198 last_error = Some(err);
199 } else {
200 tracing::warn!(%url, "session severed immediately, retrying");
201 }
202 }
203 }
204 Err(err) => {
205 if err.is_auth() {
206 return Err(err);
207 }
208 last_error = Some(err);
209 }
210 }
211
212 tracing::warn!(%url, ?delay, "reconnecting after backoff");
213 tokio::time::sleep(delay).await;
214 delay = std::cmp::min(delay * backoff.multiplier, backoff.max);
215 }
216 }
217
218 pub fn poll_status(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Status>> {
223 let last = self.last_reported;
224 let status = match ready!(self.state.poll(waiter, |state| match state.status {
225 Some(status) if Some(status) != last => Poll::Ready(status),
226 _ => Poll::Pending,
227 })) {
228 Ok(status) => status,
229 Err(state) => return Poll::Ready(Err(terminal(&state))),
230 };
231
232 self.last_reported = Some(status);
233 Poll::Ready(Ok(status))
234 }
235
236 pub async fn status(&mut self) -> crate::Result<Status> {
242 kio::wait(|waiter| self.poll_status(waiter)).await
243 }
244
245 pub fn connected(&self) -> bool {
250 self.state.read().status == Some(Status::Connected)
251 }
252
253 pub fn version(&self) -> Option<Version> {
258 self.state.read().version
259 }
260
261 pub fn send_bandwidth(&self) -> BandwidthConsumer {
268 self.send_bandwidth.clone()
269 }
270
271 pub fn recv_bandwidth(&self) -> BandwidthConsumer {
275 self.recv_bandwidth.clone()
276 }
277
278 pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<crate::Result<()>> {
283 ready!(self.state.poll_closed(waiter));
284 Poll::Ready(match &self.state.read().error {
285 Some(err) => Err(err.clone()),
286 None => Ok(()),
287 })
288 }
289
290 pub async fn closed(&self) -> crate::Result<()> {
292 kio::wait(|waiter| self.poll_closed(waiter)).await
293 }
294}
295
296async fn run_session(
304 send_bw: &BandwidthProducer,
305 recv_bw: &BandwidthProducer,
306 session: &moq_net::Session,
307) -> Result<(), moq_net::Error> {
308 let mut send = session.send_bandwidth();
309 let mut recv = session.recv_bandwidth();
310 let closed = session.closed();
311 tokio::pin!(closed);
312
313 kio::wait(|waiter| {
314 poll_forward(&mut send, send_bw, waiter);
315 poll_forward(&mut recv, recv_bw, waiter);
316 waiter.poll_future(closed.as_mut())
317 })
318 .await
319}
320
321fn poll_forward(bw: &mut Option<BandwidthConsumer>, out: &BandwidthProducer, waiter: &kio::Waiter) {
326 loop {
327 let Some(consumer) = bw.as_mut() else { return };
328 let Poll::Ready(rate) = consumer.poll_changed(waiter) else {
329 return;
330 };
331 let _ = out.set(rate);
332 if rate.is_none() {
333 *bw = None;
334 return;
335 }
336 }
337}
338
339impl Drop for Reconnect {
340 fn drop(&mut self) {
341 self.abort.abort();
342 }
343}
344
345fn terminal(state: &State) -> Error {
347 match &state.error {
348 Some(err) => err.clone(),
349 None => Error::Reconnect("reconnect stopped".to_string()),
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_backoff_default() {
359 let backoff = Backoff::default();
360 assert_eq!(backoff.initial, Duration::from_secs(1));
361 assert_eq!(backoff.multiplier, 2);
362 assert_eq!(backoff.max, Duration::from_secs(30));
363 assert_eq!(backoff.timeout, Duration::from_secs(300));
364 }
365
366 #[test]
367 fn poll_forward_mirrors_then_drops_on_none() {
368 let src = BandwidthProducer::new();
369 let out = BandwidthProducer::new();
370 let out_rx = out.consume();
371 let waiter = kio::Waiter::noop();
372
373 let mut bw = Some(src.consume());
375 poll_forward(&mut bw, &out, &waiter);
376 assert_eq!(out_rx.peek(), None);
377 assert!(bw.is_some());
378
379 src.set(Some(3_000)).unwrap();
381 poll_forward(&mut bw, &out, &waiter);
382 assert_eq!(out_rx.peek(), Some(3_000));
383
384 src.set(None).unwrap();
386 poll_forward(&mut bw, &out, &waiter);
387 assert_eq!(out_rx.peek(), None);
388 assert!(bw.is_none());
389
390 src.set(Some(9_000)).unwrap();
392 poll_forward(&mut bw, &out, &waiter);
393 assert_eq!(out_rx.peek(), None);
394 }
395}