1use std::time::{Duration, Instant};
2use std::{
3 collections::HashMap,
4 sync::{
5 Arc, Mutex,
6 atomic::{AtomicU64, Ordering},
7 },
8};
9
10use iroh_docs::NamespaceId;
11use n0_future::StreamExt;
12use tokio::{
13 sync::{broadcast, oneshot},
14 time::sleep,
15};
16
17use crate::{
18 daemon::current_timestamp,
19 error::{Result, SyncwebError},
20 filter::FilterEngine,
21 net::NetworkLogger,
22 sync::{ActiveSession, IntentControl, IntentHandle, SubscribeParams, SyncEngine, SyncEvent},
23};
24
25#[non_exhaustive]
27pub struct SupervisedIntent {
28 pub namespace: NamespaceId,
29 pub handle: Option<IntentHandle>,
30 pub retry_count: u32,
31 pub last_error: Option<String>,
32 pub last_started_at: Option<u64>,
33 pub bytes_transferred: u64,
35 pub files_transferred: u64,
37 session: Option<ActiveSession>,
38}
39
40#[derive(Clone, Debug)]
46pub struct BandwidthAccountant {
47 logger: NetworkLogger,
48 network_id: Option<String>,
49 folder: String,
50 recorded: Arc<AtomicU64>,
51}
52
53impl BandwidthAccountant {
54 #[must_use]
55 pub fn new(logger: NetworkLogger, network_id: Option<String>, folder: impl Into<String>) -> Self {
56 Self {
57 logger,
58 network_id,
59 folder: folder.into(),
60 recorded: Arc::new(AtomicU64::new(0)),
61 }
62 }
63
64 pub fn observe(&self, total_bytes: u64, files: u64) {
66 let previous = self.recorded.load(Ordering::Relaxed);
67 if total_bytes > previous
68 && let Err(error) = self.logger.record_bandwidth_download(
69 total_bytes.saturating_sub(previous),
70 files,
71 Some(&self.folder),
72 None,
73 self.network_id.as_deref(),
74 )
75 {
76 tracing::warn!(%error, "failed to record bandwidth event");
77 }
78 self.recorded.store(total_bytes.max(previous), Ordering::Relaxed);
79 }
80
81 #[must_use]
83 pub fn total(&self) -> u64 {
84 self.recorded.load(Ordering::Relaxed)
85 }
86}
87
88impl std::fmt::Debug for SupervisedIntent {
89 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 formatter
91 .debug_struct("SupervisedIntent")
92 .field("namespace", &self.namespace)
93 .field("handle", &self.handle)
94 .field("retry_count", &self.retry_count)
95 .field("last_error", &self.last_error)
96 .field("last_started_at", &self.last_started_at)
97 .field("bytes_transferred", &self.bytes_transferred)
98 .finish_non_exhaustive()
99 }
100}
101
102pub const DEFAULT_RETRY_WINDOW: Duration = Duration::from_hours(2_160);
108
109#[derive(Clone, Copy, Debug)]
112pub struct IntentSupervisor {
113 retry_window: Duration,
114 backoff_base: Duration,
115 backoff_max: Duration,
116}
117
118pub type IntentControls = Arc<Mutex<HashMap<NamespaceId, IntentControl>>>;
120
121pub struct SupervisionOptions {
123 controls: Option<IntentControls>,
124 filter: Option<FilterEngine>,
125 ready: Option<oneshot::Sender<()>>,
126 bandwidth: Option<BandwidthAccountant>,
127}
128
129impl SupervisionOptions {
130 pub const fn with_ready(
131 controls: IntentControls,
132 filter: Option<FilterEngine>,
133 ready: oneshot::Sender<()>,
134 ) -> Self {
135 Self {
136 controls: Some(controls),
137 filter,
138 ready: Some(ready),
139 bandwidth: None,
140 }
141 }
142
143 #[must_use]
145 pub fn with_bandwidth(mut self, bandwidth: BandwidthAccountant) -> Self {
146 self.bandwidth = Some(bandwidth);
147 self
148 }
149}
150
151impl IntentSupervisor {
152 #[must_use]
153 pub const fn new(retry_window: Duration, backoff_base: Duration, backoff_max: Duration) -> Self {
154 Self {
155 retry_window,
156 backoff_base,
157 backoff_max,
158 }
159 }
160
161 pub async fn run_intent(
163 &self,
164 sync: &SyncEngine,
165 namespace: NamespaceId,
166 params: SubscribeParams,
167 ) -> SupervisedIntent {
168 self.run_intent_with_filter(sync, namespace, params, None).await
169 }
170
171 pub async fn run_intent_with_filter(
173 &self,
174 sync: &SyncEngine,
175 namespace: NamespaceId,
176 params: SubscribeParams,
177 filter_engine: Option<FilterEngine>,
178 ) -> SupervisedIntent {
179 let result = match filter_engine {
180 Some(engine) => {
181 sync.sync_with_filter(namespace, crate::sync::SessionMode::Continuous, params, engine)
182 .await
183 }
184 None => sync.subscribe(namespace, params).await,
185 };
186 match result {
187 Ok(handle) => {
188 let session = ActiveSession::register(namespace, handle.cancel_sender());
189 SupervisedIntent {
190 namespace,
191 handle: Some(handle),
192 retry_count: 0,
193 last_error: None,
194 last_started_at: Some(current_timestamp()),
195 bytes_transferred: 0,
196 files_transferred: 0,
197 session: Some(session),
198 }
199 }
200 Err(error) => SupervisedIntent {
201 namespace,
202 handle: None,
203 retry_count: 0,
204 last_error: Some(error.to_string()),
205 last_started_at: None,
206 bytes_transferred: 0,
207 files_transferred: 0,
208 session: None,
209 },
210 }
211 }
212
213 pub async fn supervise(
224 &self,
225 sync: &SyncEngine,
226 namespace: NamespaceId,
227 params: SubscribeParams,
228 shutdown: broadcast::Receiver<()>,
229 ) -> Result<SupervisedIntent> {
230 self.supervise_inner(
231 sync,
232 namespace,
233 params,
234 shutdown,
235 SupervisionOptions {
236 controls: None,
237 filter: None,
238 ready: None,
239 bandwidth: None,
240 },
241 )
242 .await
243 }
244
245 pub async fn supervise_with_controls(
252 &self,
253 sync: &SyncEngine,
254 namespace: NamespaceId,
255 params: SubscribeParams,
256 shutdown: broadcast::Receiver<()>,
257 controls: IntentControls,
258 filter: Option<FilterEngine>,
259 ) -> Result<SupervisedIntent> {
260 self.supervise_inner(
261 sync,
262 namespace,
263 params,
264 shutdown,
265 SupervisionOptions {
266 controls: Some(controls),
267 filter,
268 ready: None,
269 bandwidth: None,
270 },
271 )
272 .await
273 }
274
275 pub(crate) async fn supervise_with_controls_and_ready(
276 &self,
277 sync: &SyncEngine,
278 namespace: NamespaceId,
279 params: SubscribeParams,
280 shutdown: broadcast::Receiver<()>,
281 options: SupervisionOptions,
282 ) -> Result<SupervisedIntent> {
283 self.supervise_inner(sync, namespace, params, shutdown, options).await
284 }
285
286 async fn supervise_inner(
287 &self,
288 sync: &SyncEngine,
289 namespace: NamespaceId,
290 params: SubscribeParams,
291 mut shutdown: broadcast::Receiver<()>,
292 mut options: SupervisionOptions,
293 ) -> Result<SupervisedIntent> {
294 let mut supervised = self
295 .run_intent_with_filter(sync, namespace, params.clone(), options.filter.clone())
296 .await;
297 let mut ready = options.ready.take();
298 let first_attempt = Instant::now();
299
300 loop {
301 let Some(mut handle) = supervised.handle.take() else {
302 if let Some(sender) = ready.take() {
303 let _ = sender.send(());
304 }
305 Self::remove_control(options.controls.as_ref(), namespace)?;
306 if self.retry_window_exhausted(first_attempt) {
307 tracing::warn!(
308 %namespace,
309 retry_count = supervised.retry_count,
310 window_secs = self.retry_window.as_secs(),
311 "retry window elapsed; giving up on supervised intent"
312 );
313 return Ok(supervised);
314 }
315 let retry_number = supervised.retry_count.saturating_add(1);
316 if !self
317 .wait_for_retry(self.backoff_delay(retry_number), &mut shutdown)
318 .await
319 {
320 return Ok(supervised);
321 }
322 supervised = self
323 .run_intent_with_filter(sync, namespace, params.clone(), options.filter.clone())
324 .await;
325 supervised.retry_count = retry_number;
326 continue;
327 };
328
329 Self::insert_control(options.controls.as_ref(), namespace, handle.control())?;
330 if let Some(sender) = ready.take() {
331 let _ = sender.send(());
332 }
333 let session = supervised.session.take();
334 let event = loop {
335 tokio::select! {
336 shutdown_result = shutdown.recv() => {
337 match shutdown_result {
338 Ok(()) | Err(broadcast::error::RecvError::Closed) => {
339 if let Err(error) = handle.cancel() {
340 supervised.last_error = Some(format!("failed to cancel intent: {error}"));
341 }
342 Self::remove_control(options.controls.as_ref(), namespace)?;
343 drop(session);
344 supervised.handle = None;
345 return Ok(supervised);
346 }
347 Err(broadcast::error::RecvError::Lagged(_)) => {}
348 }
349 }
350 next_event = handle.next() => break next_event,
351 }
352 };
353
354 Self::remove_control(options.controls.as_ref(), namespace)?;
355 match event {
356 Some(SyncEvent::Started) => {
357 supervised.last_error = None;
358 supervised.last_started_at = Some(current_timestamp());
359 supervised.handle = Some(handle);
360 supervised.session = session;
361 }
362 Some(SyncEvent::Cancelled | SyncEvent::Finished) => {
363 supervised.handle = None;
364 drop(session);
365 return Ok(supervised);
366 }
367 Some(SyncEvent::Failed(error)) => {
368 supervised.last_error = Some(error);
369 supervised.handle = None;
370 drop(session);
371 }
372 Some(SyncEvent::Progress { .. } | SyncEvent::Paused | SyncEvent::Resumed) => {
373 supervised.handle = Some(handle);
374 supervised.session = session;
375 }
376 Some(SyncEvent::Stats(stats)) => {
377 supervised.bytes_transferred = stats.bytes_transferred;
378 if let Some(accountant) = options.bandwidth.as_ref() {
379 accountant.observe(stats.bytes_transferred, 0);
380 }
381 supervised.handle = Some(handle);
382 supervised.session = session;
383 }
384 None => {
385 supervised.last_error = Some("synchronization intent ended unexpectedly".to_owned());
386 supervised.handle = None;
387 drop(session);
388 }
389 }
390 }
391 }
392
393 fn insert_control(controls: Option<&IntentControls>, namespace: NamespaceId, control: IntentControl) -> Result<()> {
394 let Some(control_registry) = controls else {
395 return Ok(());
396 };
397 control_registry
398 .lock()
399 .map_err(|error| SyncwebError::operation("daemon intent control mutex is poisoned", error))?
400 .insert(namespace, control);
401 Ok(())
402 }
403
404 fn remove_control(controls: Option<&IntentControls>, namespace: NamespaceId) -> Result<()> {
405 let Some(control_registry) = controls else {
406 return Ok(());
407 };
408 control_registry
409 .lock()
410 .map_err(|error| SyncwebError::operation("daemon intent control mutex is poisoned", error))?
411 .remove(&namespace);
412 Ok(())
413 }
414
415 #[must_use]
417 pub fn retry_window_exhausted(&self, first_attempt: Instant) -> bool {
418 first_attempt.elapsed() >= self.retry_window
419 }
420
421 #[must_use]
423 pub fn backoff_delay(&self, retry_number: u32) -> Duration {
424 const MIN_BACKOFF: Duration = Duration::from_millis(100);
425 if retry_number == 0 {
426 return Duration::ZERO;
427 }
428 let mut delay = self.backoff_base.max(MIN_BACKOFF);
429 for _ in 1..retry_number {
430 delay = match delay.checked_mul(2) {
431 Some(value) if value <= self.backoff_max => value,
432 _ => self.backoff_max,
433 };
434 if delay == self.backoff_max {
435 break;
436 }
437 }
438 delay.min(self.backoff_max)
439 }
440
441 async fn wait_for_retry(&self, delay: Duration, shutdown: &mut broadcast::Receiver<()>) -> bool {
442 tokio::select! {
443 () = sleep(delay) => true,
444 shutdown_result = shutdown.recv() => {
445 !matches!(shutdown_result, Ok(()) | Err(broadcast::error::RecvError::Closed))
446 }
447 }
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn backoff_delay_starts_at_base() {
457 let supervisor = IntentSupervisor::new(
458 Duration::from_hours(2_160),
459 Duration::from_secs(1),
460 Duration::from_mins(1),
461 );
462 assert_eq!(supervisor.backoff_delay(1), Duration::from_secs(1));
463 }
464
465 #[test]
466 fn backoff_delay_doubles() {
467 let supervisor = IntentSupervisor::new(
468 Duration::from_hours(2_160),
469 Duration::from_secs(1),
470 Duration::from_mins(1),
471 );
472 assert_eq!(supervisor.backoff_delay(2), Duration::from_secs(2));
473 assert_eq!(supervisor.backoff_delay(3), Duration::from_secs(4));
474 }
475
476 #[test]
477 fn backoff_delay_capped() {
478 let supervisor = IntentSupervisor::new(
479 Duration::from_hours(2_160),
480 Duration::from_secs(1),
481 Duration::from_secs(3),
482 );
483 assert_eq!(supervisor.backoff_delay(2), Duration::from_secs(2));
484 assert_eq!(supervisor.backoff_delay(3), Duration::from_secs(3));
485 assert_eq!(supervisor.backoff_delay(10), Duration::from_secs(3));
486 }
487
488 #[test]
489 fn backoff_delay_zero_retry() {
490 let supervisor = IntentSupervisor::new(
491 Duration::from_hours(2_160),
492 Duration::from_secs(1),
493 Duration::from_mins(1),
494 );
495 assert_eq!(supervisor.backoff_delay(0), Duration::ZERO);
496 }
497
498 #[test]
499 fn backoff_delay_zero_base() {
500 let supervisor = IntentSupervisor::new(Duration::from_hours(2_160), Duration::ZERO, Duration::from_mins(1));
501 assert_eq!(supervisor.backoff_delay(1), Duration::from_millis(100));
502 assert_eq!(supervisor.backoff_delay(5), Duration::from_millis(1600));
503 }
504
505 #[test]
506 fn auto_backoff_delay_respects_max_retries() {
507 let supervisor = IntentSupervisor::new(
508 Duration::from_hours(2_160),
509 Duration::from_secs(2),
510 Duration::from_secs(20),
511 );
512 let delays: Vec<_> = (1..=8).map(|n| supervisor.backoff_delay(n)).collect();
513 let expected = [
514 Duration::from_secs(2),
515 Duration::from_secs(4),
516 Duration::from_secs(8),
517 Duration::from_secs(16),
518 Duration::from_secs(20),
519 Duration::from_secs(20),
520 Duration::from_secs(20),
521 Duration::from_secs(20),
522 ];
523 for (delay, want) in delays.iter().zip(expected.iter()) {
524 assert_eq!(delay, want);
525 }
526 }
527
528 #[test]
529 fn backoff_delay_starting_value_equals_base() {
530 let supervisor = IntentSupervisor::new(
531 Duration::from_hours(2_160),
532 Duration::from_millis(500),
533 Duration::from_secs(5),
534 );
535 assert_eq!(supervisor.backoff_delay(1), Duration::from_millis(500));
536 }
537
538 #[test]
539 fn retry_window_is_measured_from_first_attempt() {
540 let supervisor = IntentSupervisor::new(Duration::from_mins(1), Duration::from_secs(1), Duration::from_mins(1));
541 let now = Instant::now();
542 assert!(!supervisor.retry_window_exhausted(now));
543 let started_ago = now.checked_sub(Duration::from_secs(61)).expect("instant underflow");
544 assert!(supervisor.retry_window_exhausted(started_ago));
545 assert_eq!(DEFAULT_RETRY_WINDOW, Duration::from_hours(2_160));
546 }
547}