1use std::task::{Poll, ready};
2use std::time::Duration;
3
4use moq_net::Version;
5use moq_net::bandwidth::{Consumer as BandwidthConsumer, Producer as BandwidthProducer};
6use moq_net::kio;
7use url::Url;
8
9use crate::{Client, Error};
10
11#[derive(Clone, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
13#[serde(default, deny_unknown_fields)]
14#[non_exhaustive]
15pub struct Backoff {
16 #[arg(
18 id = "backoff-initial",
19 long,
20 default_value = "1s",
21 env = "MOQ_BACKOFF_INITIAL",
22 value_parser = humantime::parse_duration,
23 )]
24 #[serde(with = "humantime_serde")]
25 pub initial: Duration,
26
27 #[arg(id = "backoff-multiplier", long, default_value_t = 2, env = "MOQ_BACKOFF_MULTIPLIER")]
29 pub multiplier: u32,
30
31 #[arg(
33 id = "backoff-max",
34 long,
35 default_value = "30s",
36 env = "MOQ_BACKOFF_MAX",
37 value_parser = humantime::parse_duration,
38 )]
39 #[serde(with = "humantime_serde")]
40 pub max: Duration,
41
42 #[arg(
47 id = "backoff-timeout",
48 long,
49 default_value = "5m",
50 env = "MOQ_BACKOFF_TIMEOUT",
51 value_parser = humantime::parse_duration,
52 )]
53 #[serde(with = "humantime_serde")]
54 pub timeout: Duration,
55}
56
57impl Default for Backoff {
58 fn default() -> Self {
59 Self {
60 initial: Duration::from_secs(1),
61 multiplier: 2,
62 max: Duration::from_secs(30),
63 timeout: Duration::from_secs(300),
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum Status {
72 Connected,
74 Disconnected,
76}
77
78#[derive(Default)]
83struct State {
84 status: Option<Status>,
86 version: Option<Version>,
88 error: Option<Error>,
90 session: Option<moq_net::Session>,
93}
94
95#[derive(Clone)]
100pub struct ConnectionStatsReader {
101 state: kio::Consumer<State>,
102}
103
104impl ConnectionStatsReader {
105 pub fn stats(&self) -> Option<moq_net::ConnectionStats> {
107 self.state.read().session.as_ref().map(moq_net::Session::stats)
108 }
109}
110
111pub struct Reconnect {
121 abort: tokio::task::AbortHandle,
122 state: kio::Consumer<State>,
123 send_bandwidth: BandwidthConsumer,
125 recv_bandwidth: BandwidthConsumer,
127 last_reported: Option<Status>,
129}
130
131impl Reconnect {
132 pub(crate) fn new(client: Client, url: Url, backoff: Backoff) -> Self {
133 let producer = kio::Producer::<State>::default();
134 let state = producer.consume();
135
136 let send_bw = BandwidthProducer::new();
139 let recv_bw = BandwidthProducer::new();
140 let send_bandwidth = send_bw.consume();
141 let recv_bandwidth = recv_bw.consume();
142
143 let task = tokio::spawn(async move {
144 if let Err(err) = Self::run(&producer, &send_bw, &recv_bw, client, url, backoff).await {
145 tracing::error!(%err, "reconnect loop exited");
146 if let Ok(mut state) = producer.write() {
147 state.error = Some(err);
148 }
149 }
150 });
152 Self {
153 abort: task.abort_handle(),
154 state,
155 send_bandwidth,
156 recv_bandwidth,
157 last_reported: None,
158 }
159 }
160
161 async fn run(
162 state: &kio::Producer<State>,
163 send_bw: &BandwidthProducer,
164 recv_bw: &BandwidthProducer,
165 client: Client,
166 url: Url,
167 backoff: Backoff,
168 ) -> crate::Result<()> {
169 let mut delay = backoff.initial;
170 let mut retry_start = tokio::time::Instant::now();
171 let mut last_error: Option<Error> = None;
172
173 loop {
174 if !backoff.timeout.is_zero() && retry_start.elapsed() > backoff.timeout {
175 let timeout = backoff.timeout;
176 let msg = match last_error {
177 Some(err) => format!("reconnect timed out after {timeout:?}: {err}"),
178 None => format!("reconnect timed out after {timeout:?}"),
179 };
180 return Err(Error::Reconnect(msg));
181 }
182
183 tracing::info!(%url, "connecting");
184
185 match client.connect(url.clone()).await {
186 Ok(session) => {
187 tracing::info!(%url, "connected");
188 if let Ok(mut state) = state.write() {
189 state.status = Some(Status::Connected);
190 state.version = Some(session.version());
191 state.session = Some(session.clone());
192 }
193
194 let connected = tokio::time::Instant::now();
195 let closed = run_session(send_bw, recv_bw, &session).await;
198 if let Ok(mut state) = state.write() {
199 state.status = Some(Status::Disconnected);
200 state.version = None;
201 state.session = None;
202 }
203 let _ = send_bw.set(None);
205 let _ = recv_bw.set(None);
206
207 if connected.elapsed() >= backoff.initial {
208 tracing::warn!(%url, "session closed, reconnecting");
211 delay = backoff.initial;
212 retry_start = tokio::time::Instant::now();
213 last_error = None;
214 } else {
215 if let Err(err) = closed {
220 let err = Error::from(err);
221 tracing::warn!(%url, %err, "session severed immediately, retrying");
222 last_error = Some(err);
223 } else {
224 tracing::warn!(%url, "session severed immediately, retrying");
225 }
226 }
227 }
228 Err(err) => {
229 if err.is_auth() {
230 return Err(err);
231 }
232 last_error = Some(err);
233 }
234 }
235
236 tracing::warn!(%url, ?delay, "reconnecting after backoff");
237 tokio::time::sleep(delay).await;
238 delay = std::cmp::min(delay * backoff.multiplier, backoff.max);
239 }
240 }
241
242 pub fn poll_status(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Status>> {
247 let last = self.last_reported;
248 let status = match ready!(self.state.poll(waiter, |state| match state.status {
249 Some(status) if Some(status) != last => Poll::Ready(status),
250 _ => Poll::Pending,
251 })) {
252 Ok(status) => status,
253 Err(state) => return Poll::Ready(Err(terminal(&state))),
254 };
255
256 self.last_reported = Some(status);
257 Poll::Ready(Ok(status))
258 }
259
260 pub async fn status(&mut self) -> crate::Result<Status> {
266 kio::wait(|waiter| self.poll_status(waiter)).await
267 }
268
269 pub fn connected(&self) -> bool {
274 self.state.read().status == Some(Status::Connected)
275 }
276
277 pub fn version(&self) -> Option<Version> {
282 self.state.read().version
283 }
284
285 pub fn send_bandwidth(&self) -> BandwidthConsumer {
292 self.send_bandwidth.clone()
293 }
294
295 pub fn recv_bandwidth(&self) -> BandwidthConsumer {
299 self.recv_bandwidth.clone()
300 }
301
302 pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<crate::Result<()>> {
307 ready!(self.state.poll_closed(waiter));
308 Poll::Ready(match &self.state.read().error {
309 Some(err) => Err(err.clone()),
310 None => Ok(()),
311 })
312 }
313
314 pub async fn closed(&self) -> crate::Result<()> {
316 kio::wait(|waiter| self.poll_closed(waiter)).await
317 }
318
319 pub fn stats(&self) -> ConnectionStatsReader {
323 ConnectionStatsReader {
324 state: self.state.clone(),
325 }
326 }
327}
328
329async fn run_session(
337 send_bw: &BandwidthProducer,
338 recv_bw: &BandwidthProducer,
339 session: &moq_net::Session,
340) -> Result<(), moq_net::Error> {
341 let mut send = session.send_bandwidth();
342 let mut recv = session.recv_bandwidth();
343 let closed = session.closed();
344 tokio::pin!(closed);
345
346 let err = kio::wait(|waiter| {
347 poll_forward(&mut send, send_bw, waiter);
348 poll_forward(&mut recv, recv_bw, waiter);
349 waiter.poll_future(closed.as_mut())
350 })
351 .await;
352
353 Err(err)
354}
355
356fn poll_forward(bw: &mut Option<BandwidthConsumer>, out: &BandwidthProducer, waiter: &kio::Waiter) {
364 loop {
365 let Some(consumer) = bw.as_mut() else { return };
366 let Poll::Ready(res) = consumer.poll_changed(waiter) else {
367 return;
368 };
369 match res {
370 Ok(rate) => {
371 let _ = out.set(rate);
372 }
373 Err(_) => {
374 *bw = None;
375 return;
376 }
377 }
378 }
379}
380
381impl Drop for Reconnect {
382 fn drop(&mut self) {
383 self.abort.abort();
384 }
385}
386
387fn terminal(state: &State) -> Error {
389 match &state.error {
390 Some(err) => err.clone(),
391 None => Error::Reconnect("reconnect stopped".to_string()),
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_backoff_default() {
401 let backoff = Backoff::default();
402 assert_eq!(backoff.initial, Duration::from_secs(1));
403 assert_eq!(backoff.multiplier, 2);
404 assert_eq!(backoff.max, Duration::from_secs(30));
405 assert_eq!(backoff.timeout, Duration::from_secs(300));
406 }
407
408 #[test]
409 fn poll_forward_mirrors_until_the_source_closes() {
410 let src = BandwidthProducer::new();
411 let out = BandwidthProducer::new();
412 let out_rx = out.consume();
413 let waiter = kio::Waiter::noop();
414
415 let mut bw = Some(src.consume());
417 poll_forward(&mut bw, &out, &waiter);
418 assert_eq!(out_rx.peek(), None);
419 assert!(bw.is_some());
420
421 src.set(Some(3_000)).unwrap();
423 poll_forward(&mut bw, &out, &waiter);
424 assert_eq!(out_rx.peek(), Some(3_000));
425
426 src.set(None).unwrap();
429 poll_forward(&mut bw, &out, &waiter);
430 assert_eq!(out_rx.peek(), None);
431 assert!(bw.is_some());
432
433 src.set(Some(9_000)).unwrap();
437 poll_forward(&mut bw, &out, &waiter);
438 assert_eq!(out_rx.peek(), Some(9_000));
439
440 src.abort(moq_net::Error::Cancel).unwrap();
442 poll_forward(&mut bw, &out, &waiter);
443 assert!(bw.is_none());
444 }
445}