1use std::collections::HashMap;
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::{Arc, Mutex, PoisonError, Weak};
13use std::time::{Duration, SystemTime};
14
15use tokio::sync::mpsc;
16use tokio::task::{AbortHandle, Id, JoinError, JoinSet};
17use tokio_util::sync::CancellationToken;
18
19pub const DEFAULT_START: Duration = Duration::from_secs(1);
21pub const DEFAULT_CAP: Duration = Duration::from_secs(60);
23pub const DEFAULT_GIVE_UP_AFTER: u32 = 10;
25pub const DEFAULT_RESET_AFTER: Duration = Duration::from_secs(60);
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Policy {
31 Never,
33 Backoff { start: Duration, cap: Duration, give_up_after: u32, reset_after: Duration },
35}
36
37impl Policy {
38 pub fn backoff() -> Self {
41 Self::Backoff {
42 start: DEFAULT_START,
43 cap: DEFAULT_CAP,
44 give_up_after: DEFAULT_GIVE_UP_AFTER,
45 reset_after: DEFAULT_RESET_AFTER,
46 }
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum MemberState {
53 Running { since: SystemTime },
56 GivenUp { after: u32 },
58 Stopped,
61}
62
63#[derive(Clone, Debug)]
65pub struct Member {
66 pub name: String,
67 pub policy: Policy,
68 pub state: MemberState,
69 pub restarts: u32,
70 pub last_error: Option<String>,
71}
72
73#[derive(Clone)]
76pub struct Supervisor(Arc<Inner>);
77
78impl Supervisor {
79 pub fn new() -> Self {
85 let (tx, rx) = mpsc::unbounded_channel();
86 let inner =
87 Arc::new(Inner { tx, rx: Mutex::new(Some(rx)), state: Mutex::new(State::default()) });
88 inner.ensure_driver();
89 Self(inner)
90 }
91
92 pub fn spawn<F, Fut>(&self, name: &str, policy: Policy, factory: F) -> bool
101 where
102 F: Fn() -> Fut + Send + Sync + 'static,
103 Fut: Future<Output = ()> + Send + 'static,
104 {
105 self.0.ensure_driver();
106 let factory: Factory = Arc::new(move || Box::pin(factory()));
107 let mut state = self.0.state.lock().unwrap_or_else(PoisonError::into_inner);
108 if state
109 .members
110 .get(name)
111 .is_some_and(|member| matches!(&member.state, MemberState::Running { .. }))
112 {
113 return false;
114 }
115 let generation = state.generations.get(name).copied().unwrap_or(0) + 1;
116 let token = CancellationToken::new();
117 state.generations.insert(name.to_owned(), generation);
118 state.tokens.insert(name.to_owned(), token.clone());
119 state.factories.insert(name.to_owned(), factory);
120 state.members.insert(
121 name.to_owned(),
122 Member {
123 name: name.to_owned(),
124 policy,
125 state: MemberState::Running { since: SystemTime::now() },
126 restarts: 0,
127 last_error: None,
128 },
129 );
130 drop(state);
131 let _ = self.0.tx.send(Command::Start { name: name.to_owned(), generation, token });
132 true
133 }
134
135 pub fn stop(&self, name: &str) {
141 let mut state = self.0.state.lock().unwrap_or_else(PoisonError::into_inner);
142 let Some(member) = state.members.get_mut(name) else { return };
143 member.state = MemberState::Stopped;
144 if let Some(token) = state.tokens.get(name) {
145 token.cancel();
146 }
147 let generation = state.generations.get(name).copied().unwrap_or(0);
148 drop(state);
149 let _ = self.0.tx.send(Command::Stop { name: name.to_owned(), generation });
150 tracing::debug!(
151 target: "forge_supervise",
152 event_name = "subsystem_stopped",
153 subsystem = %name,
154 "the subsystem was stopped",
155 );
156 }
157
158 pub fn forget(&self, name: &str) {
162 let aborts: Vec<AbortHandle> = {
163 let mut state = self.0.state.lock().unwrap_or_else(PoisonError::into_inner);
164 if let Some(token) = state.tokens.remove(name) {
165 token.cancel();
166 }
167 state.members.remove(name);
168 state.factories.remove(name);
169 let keys: Vec<(String, u64)> = state
174 .aborts
175 .keys()
176 .filter(|(member, _)| member.as_str() == name)
177 .cloned()
178 .collect();
179 keys.into_iter().filter_map(|key| state.aborts.remove(&key)).collect()
180 };
181 for abort in aborts {
182 abort.abort();
183 }
184 }
185
186 pub fn snapshot(&self) -> Vec<Member> {
188 let state = self.0.state.lock().unwrap_or_else(PoisonError::into_inner);
189 let mut members: Vec<Member> = state.members.values().cloned().collect();
190 members.sort_by(|a, b| a.name.cmp(&b.name));
191 members
192 }
193}
194
195impl Default for Supervisor {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201type Factory = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
205
206enum Command {
207 Start { name: String, generation: u64, token: CancellationToken },
208 Restart { name: String, generation: u64, token: CancellationToken, reason: String },
209 Stop { name: String, generation: u64 },
210}
211
212struct Run {
213 name: String,
214 generation: u64,
215 token: CancellationToken,
216 started_at: tokio::time::Instant,
219}
220
221struct Inner {
222 tx: mpsc::UnboundedSender<Command>,
223 rx: Mutex<Option<mpsc::UnboundedReceiver<Command>>>,
224 state: Mutex<State>,
225}
226
227#[derive(Default)]
228struct State {
229 members: HashMap<String, Member>,
230 factories: HashMap<String, Factory>,
231 tokens: HashMap<String, CancellationToken>,
232 generations: HashMap<String, u64>,
233 aborts: HashMap<(String, u64), AbortHandle>,
236}
237
238impl Inner {
239 fn ensure_driver(self: &Arc<Self>) {
242 let mut slot = self.rx.lock().unwrap_or_else(PoisonError::into_inner);
243 let Some(rx) = slot.take() else { return };
244 match tokio::runtime::Handle::try_current() {
245 Ok(handle) => {
246 handle.spawn(drive(Arc::downgrade(self), rx));
247 }
248 Err(_) => {
249 *slot = Some(rx);
250 }
251 }
252 }
253}
254
255async fn drive(weak: Weak<Inner>, mut rx: mpsc::UnboundedReceiver<Command>) {
258 let mut set = JoinSet::new();
259 let mut runs: HashMap<Id, Run> = HashMap::new();
260 loop {
261 tokio::select! {
262 command = rx.recv() => {
263 let Some(command) = command else { break };
264 let Some(inner) = weak.upgrade() else { break };
265 match command {
266 Command::Start { name, generation, token } => {
267 start_run(&inner, &mut set, &mut runs, &name, generation, token, None);
268 }
269 Command::Restart { name, generation, token, reason } => {
270 start_run(
271 &inner,
272 &mut set,
273 &mut runs,
274 &name,
275 generation,
276 token,
277 Some(&reason),
278 );
279 }
280 Command::Stop { name, generation } => {
281 abort_run(&inner, &name, generation);
282 }
283 }
284 }
285 joined = set.join_next_with_id(), if !set.is_empty() => {
289 let Some(joined) = joined else { continue };
290 let Some(inner) = weak.upgrade() else { break };
291 match joined {
292 Ok((id, ())) => on_exit(&inner, &mut runs, id, None),
293 Err(error) => {
294 let id = error.id();
295 on_exit(&inner, &mut runs, id, Some(error));
296 }
297 }
298 }
299 }
300 }
301}
302
303fn start_run(
306 inner: &Arc<Inner>,
307 set: &mut JoinSet<()>,
308 runs: &mut HashMap<Id, Run>,
309 name: &str,
310 generation: u64,
311 token: CancellationToken,
312 restart: Option<&str>,
313) {
314 if token.is_cancelled() {
318 return;
319 }
320 let (factory, restarts) = {
321 let mut state = inner.state.lock().unwrap_or_else(PoisonError::into_inner);
322 if state.generations.get(name).copied() != Some(generation) {
323 return;
324 }
325 if restart.is_some()
326 && let Some(member) = state.members.get_mut(name)
327 {
328 member.state = MemberState::Running { since: SystemTime::now() };
329 }
330 let factory = state.factories.get(name).cloned();
331 let restarts = state.members.get(name).map_or(0, |member| member.restarts);
332 (factory, restarts)
333 };
334 let Some(factory) = factory else { return };
338 let handle = set.spawn(factory());
339 let id = handle.id();
340 runs.insert(
341 id,
342 Run { name: name.to_owned(), generation, token, started_at: tokio::time::Instant::now() },
343 );
344 let mut state = inner.state.lock().unwrap_or_else(PoisonError::into_inner);
345 state.aborts.insert((name.to_owned(), generation), handle);
346 drop(state);
347 if let Some(reason) = restart {
348 tracing::warn!(
349 target: "forge_supervise",
350 event_name = "subsystem_restarted",
351 subsystem = %name,
352 reason = %reason,
353 restarts,
354 "the subsystem was restarted",
355 );
356 } else {
357 tracing::debug!(
358 target: "forge_supervise",
359 event_name = "subsystem_started",
360 subsystem = %name,
361 "the subsystem started",
362 );
363 }
364}
365
366fn abort_run(inner: &Arc<Inner>, name: &str, generation: u64) {
370 let abort = {
371 let state = inner.state.lock().unwrap_or_else(PoisonError::into_inner);
372 state.aborts.get(&(name.to_owned(), generation)).cloned()
373 };
374 if let Some(abort) = abort {
375 abort.abort();
376 }
377}
378
379fn on_exit(inner: &Arc<Inner>, runs: &mut HashMap<Id, Run>, id: Id, error: Option<JoinError>) {
382 let Some(run) = runs.remove(&id) else { return };
383 let ran_for = run.started_at.elapsed();
384 let stopped = run.token.is_cancelled();
385 let clean = error.is_none();
386 let reason = error_reason(error);
387 let mut state = inner.state.lock().unwrap_or_else(PoisonError::into_inner);
388 state.aborts.remove(&(run.name.clone(), run.generation));
389 if stopped {
390 return;
391 }
392 if state.generations.get(&run.name).copied() != Some(run.generation) {
393 return;
394 }
395 let Some(member) = state.members.get_mut(&run.name) else { return };
396 match member.policy {
397 Policy::Never => {
398 if clean {
403 member.state = MemberState::Stopped;
404 member.last_error = None;
405 tracing::debug!(
406 target: "forge_supervise",
407 event_name = "subsystem_exited",
408 subsystem = %run.name,
409 "the subsystem exited",
410 );
411 } else {
412 member.state = MemberState::GivenUp { after: 0 };
413 member.last_error = Some(reason.clone());
414 tracing::warn!(
415 target: "forge_supervise",
416 event_name = "subsystem_exited",
417 subsystem = %run.name,
418 reason = %reason,
419 "the subsystem exited and is not restarted",
420 );
421 }
422 }
423 Policy::Backoff { start, cap, give_up_after, reset_after } => {
424 member.last_error = Some(reason.clone());
425 if ran_for >= reset_after {
426 member.restarts = 0;
427 }
428 if member.restarts + 1 >= give_up_after {
429 member.state = MemberState::GivenUp { after: member.restarts };
430 tracing::error!(
431 target: "forge_supervise",
432 event_name = "subsystem_gave_up",
433 subsystem = %run.name,
434 restarts = member.restarts,
435 reason = %reason,
436 "the subsystem died again after its restart budget; it is left down",
437 );
438 } else {
439 member.restarts += 1;
440 let delay = backoff(start, cap, member.restarts);
441 tracing::warn!(
442 target: "forge_supervise",
443 event_name = "subsystem_exited",
444 subsystem = %run.name,
445 reason = %reason,
446 restart_in_ms = delay.as_millis(),
447 "the subsystem died; it will be restarted",
448 );
449 let weak = Arc::downgrade(inner);
450 let name = run.name.clone();
451 let generation = run.generation;
452 let token = run.token.clone();
453 tokio::spawn(async move {
454 tokio::time::sleep(delay).await;
455 if let Some(inner) = weak.upgrade() {
456 let _ = inner.tx.send(Command::Restart { name, generation, token, reason });
457 }
458 });
459 }
460 }
461 }
462}
463
464fn backoff(start: Duration, cap: Duration, restarts: u32) -> Duration {
466 let factor = 1u32.checked_shl(restarts.saturating_sub(1)).unwrap_or(u32::MAX);
467 start.saturating_mul(factor).min(cap)
468}
469
470fn error_reason(error: Option<JoinError>) -> String {
472 match error {
473 Some(error) if error.is_panic() => {
474 let payload = error.into_panic();
475 let text = payload
476 .downcast_ref::<&str>()
477 .map(|text| (*text).to_owned())
478 .or_else(|| payload.downcast_ref::<String>().cloned())
479 .unwrap_or_else(|| "a panic with no message".to_owned());
480 format!("panicked: {text}")
481 }
482 Some(error) if error.is_cancelled() => "cancelled".to_owned(),
483 Some(error) => format!("the task failed: {error}"),
484 None => "the task returned".to_owned(),
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
491
492 use super::*;
493
494 fn counting(
497 runs: Arc<AtomicU32>,
498 ) -> impl Fn() -> std::future::Ready<()> + Send + Sync + 'static {
499 move || {
500 runs.fetch_add(1, Ordering::SeqCst);
501 std::future::ready(())
502 }
503 }
504
505 fn member(supervisor: &Supervisor, name: &str) -> Member {
506 supervisor
507 .snapshot()
508 .into_iter()
509 .find(|member| member.name == name)
510 .expect("the member is registered")
511 }
512
513 #[tokio::test(start_paused = true)]
516 async fn an_immediate_return_restarts_per_policy() {
517 let supervisor = Supervisor::new();
518 let runs = Arc::new(AtomicU32::new(0));
519 assert!(supervisor.spawn("probe", Policy::backoff(), counting(Arc::clone(&runs))));
520
521 tokio::task::yield_now().await;
522 tokio::task::yield_now().await;
523 assert_eq!(runs.load(Ordering::SeqCst), 1, "the first run started");
524
525 tokio::time::advance(DEFAULT_START).await;
526 tokio::task::yield_now().await;
527 tokio::task::yield_now().await;
528 assert_eq!(runs.load(Ordering::SeqCst), 2, "the immediate return restarted once");
529 }
530
531 #[test]
535 fn backoff_doubles_then_caps() {
536 let start = Duration::from_secs(1);
537 let cap = Duration::from_secs(60);
538 let waits: Vec<u64> =
539 (1..=8).map(|restarts| backoff(start, cap, restarts).as_secs()).collect();
540 assert_eq!(waits, vec![1, 2, 4, 8, 16, 32, 60, 60], "start doubles and stops at the cap");
541 }
542
543 #[tokio::test(start_paused = true)]
546 async fn a_second_spawn_while_running_is_a_no_op() {
547 let supervisor = Supervisor::new();
548 let runs = Arc::new(AtomicU32::new(0));
549 assert!(
550 supervisor.spawn("probe", Policy::backoff(), counting(Arc::clone(&runs))),
551 "the first starts"
552 );
553 assert!(
554 !supervisor.spawn("probe", Policy::backoff(), counting(Arc::clone(&runs))),
555 "the second is a no-op",
556 );
557 tokio::task::yield_now().await;
558 tokio::task::yield_now().await;
559 assert_eq!(runs.load(Ordering::SeqCst), 1, "only one run started");
560 assert_eq!(supervisor.snapshot().len(), 1, "one member in the registry");
561 }
562
563 #[tokio::test(start_paused = true)]
567 async fn a_panicking_task_gives_up_after_the_budget() {
568 let supervisor = Supervisor::new();
569 let policy = Policy::Backoff {
570 start: Duration::from_millis(1),
571 cap: Duration::from_millis(1),
572 give_up_after: 3,
573 reset_after: DEFAULT_RESET_AFTER,
574 };
575 let (tx, mut rx) = mpsc::unbounded_channel();
576 assert!(supervisor.spawn("boom", policy, move || {
577 let tx = tx.clone();
578 async move {
579 let _ = tx.send(());
580 panic!("the pump died");
581 }
582 }));
583
584 for run in 0..3 {
588 rx.recv().await.expect("a run started");
589 if run < 2 {
590 tokio::time::advance(Duration::from_millis(1)).await;
591 }
592 }
593 tokio::task::yield_now().await;
594
595 let member = member(&supervisor, "boom");
596 assert!(
597 matches!(member.state, MemberState::GivenUp { .. }),
598 "the budget is spent: {:?}",
599 member.state,
600 );
601 assert!(
602 member.last_error.as_deref().is_some_and(|error| error.contains("the pump died")),
603 "the panic payload is recorded: {:?}",
604 member.last_error,
605 );
606 }
607
608 #[tokio::test(start_paused = true)]
611 async fn a_spawn_after_give_up_starts_a_new_run() {
612 let supervisor = Supervisor::new();
613 let policy = Policy::Backoff {
614 start: Duration::from_millis(1),
615 cap: Duration::from_millis(1),
616 give_up_after: 1,
617 reset_after: DEFAULT_RESET_AFTER,
618 };
619 let runs = Arc::new(AtomicU32::new(0));
620 assert!(supervisor.spawn("probe", policy, counting(Arc::clone(&runs))));
621 tokio::task::yield_now().await;
622 tokio::task::yield_now().await;
623 assert!(
624 matches!(member(&supervisor, "probe").state, MemberState::GivenUp { .. }),
625 "the budget is spent",
626 );
627
628 let (tx, mut rx) = mpsc::unbounded_channel();
631 assert!(
632 supervisor.spawn("probe", Policy::backoff(), move || {
633 let tx = tx.clone();
634 async move {
635 let _ = tx.send(());
636 std::future::pending::<()>().await;
637 }
638 }),
639 "a give-up can be re-spawned",
640 );
641 rx.recv().await.expect("the new run started");
642 assert_eq!(member(&supervisor, "probe").restarts, 0, "the new run resets the counter");
643 assert_eq!(runs.load(Ordering::SeqCst), 1, "the first factory ran once");
644 }
645
646 #[tokio::test(start_paused = true)]
651 async fn a_stop_then_spawn_aborts_the_run_it_stopped() {
652 struct Dropped(Arc<AtomicBool>);
653 impl Drop for Dropped {
654 fn drop(&mut self) {
655 self.0.store(true, Ordering::SeqCst);
656 }
657 }
658
659 let supervisor = Supervisor::new();
660 let (started_tx, mut started_rx) = mpsc::unbounded_channel();
661 let first_freed = Arc::new(AtomicBool::new(false));
662 let freed = Arc::clone(&first_freed);
663 assert!(supervisor.spawn("probe", Policy::backoff(), move || {
664 let started_tx = started_tx.clone();
665 let freed = Arc::clone(&freed);
666 async move {
667 let _dropped = Dropped(freed);
668 let _ = started_tx.send(());
669 std::future::pending::<()>().await;
670 }
671 }));
672 started_rx.recv().await.expect("the first run started");
673
674 supervisor.stop("probe");
675 assert!(supervisor.spawn("probe", Policy::backoff(), || async {
679 std::future::pending::<()>().await;
680 }));
681 for _ in 0..10 {
682 tokio::task::yield_now().await;
683 }
684
685 assert!(
686 first_freed.load(Ordering::SeqCst),
687 "the stopped run was aborted even though a new run took its name",
688 );
689 assert!(
690 matches!(member(&supervisor, "probe").state, MemberState::Running { .. }),
691 "the new run is the one alive",
692 );
693 }
694
695 #[tokio::test(start_paused = true)]
698 async fn a_never_member_that_returns_is_stopped() {
699 let supervisor = Supervisor::new();
700 let (tx, mut rx) = mpsc::unbounded_channel();
701 assert!(supervisor.spawn("probe", Policy::Never, move || {
702 let tx = tx.clone();
703 async move {
704 let _ = tx.send(());
705 }
706 }));
707 rx.recv().await.expect("the run started");
708 tokio::task::yield_now().await;
709
710 let member = member(&supervisor, "probe");
711 assert_eq!(member.state, MemberState::Stopped, "a clean return is not a give-up");
712 assert_eq!(member.restarts, 0, "nothing was restarted");
713 assert!(
714 member.last_error.is_none(),
715 "a clean return records no error: {:?}",
716 member.last_error,
717 );
718 }
719
720 #[tokio::test(start_paused = true)]
723 async fn a_never_member_that_panics_is_given_up() {
724 let supervisor = Supervisor::new();
725 let (tx, mut rx) = mpsc::unbounded_channel();
726 assert!(supervisor.spawn("boom", Policy::Never, move || {
727 let tx = tx.clone();
728 async move {
729 let _ = tx.send(());
730 panic!("the report-only member died");
731 }
732 }));
733 rx.recv().await.expect("the run started");
734 tokio::task::yield_now().await;
735
736 let member = member(&supervisor, "boom");
737 assert!(matches!(member.state, MemberState::GivenUp { .. }), "a panic is a death");
738 assert!(
739 member
740 .last_error
741 .as_deref()
742 .is_some_and(|error| error.contains("the report-only member died")),
743 "the panic payload is recorded: {:?}",
744 member.last_error,
745 );
746 }
747
748 #[tokio::test(start_paused = true)]
754 async fn a_stopped_member_is_not_restarted() {
755 let supervisor = Supervisor::new();
756 let (tx, mut rx) = mpsc::unbounded_channel();
757 assert!(supervisor.spawn("probe", Policy::backoff(), move || {
758 let tx = tx.clone();
759 async move {
760 let _ = tx.send(());
761 tokio::time::sleep(Duration::from_millis(10)).await;
762 }
763 }));
764 rx.recv().await.expect("the run started");
765
766 supervisor.stop("probe");
767 tokio::time::advance(Duration::from_secs(120)).await;
771 tokio::task::yield_now().await;
772 tokio::time::advance(Duration::from_secs(120)).await;
773 tokio::task::yield_now().await;
774
775 assert!(rx.try_recv().is_err(), "a stopped member is never resurrected");
776 let member = member(&supervisor, "probe");
777 assert_eq!(member.state, MemberState::Stopped, "the stop is recorded");
778 assert_eq!(member.restarts, 0, "a stop is not a failed attempt");
779 assert!(member.last_error.is_none(), "a stop is not a death");
780 }
781
782 #[tokio::test(start_paused = true)]
786 async fn a_stop_during_backoff_is_not_resurrected() {
787 let supervisor = Supervisor::new();
788 let (tx, mut rx) = mpsc::unbounded_channel();
789 assert!(supervisor.spawn("probe", Policy::backoff(), move || {
790 let tx = tx.clone();
791 async move {
792 let _ = tx.send(());
793 }
794 }));
795 rx.recv().await.expect("the first run started");
797 tokio::task::yield_now().await;
798
799 supervisor.stop("probe");
800 tokio::time::advance(Duration::from_secs(120)).await;
801 tokio::task::yield_now().await;
802
803 assert!(rx.try_recv().is_err(), "a stop during backoff is never resurrected");
804 assert_eq!(
805 member(&supervisor, "probe").state,
806 MemberState::Stopped,
807 "the stop is recorded"
808 );
809 }
810
811 #[tokio::test(start_paused = true)]
814 async fn a_run_that_outlives_reset_after_resets_the_counter() {
815 let supervisor = Supervisor::new();
816 let policy = Policy::Backoff {
817 start: Duration::from_millis(1),
818 cap: Duration::from_millis(1),
819 give_up_after: 5,
820 reset_after: Duration::from_secs(10),
821 };
822 let (tx, mut rx) = mpsc::unbounded_channel();
823 let runs = Arc::new(AtomicU32::new(0));
824 let counter = Arc::clone(&runs);
825 assert!(supervisor.spawn("probe", policy, move || {
826 let tx = tx.clone();
827 let counter = Arc::clone(&counter);
828 async move {
829 let run = counter.fetch_add(1, Ordering::SeqCst);
830 let _ = tx.send(run);
831 match run {
832 0 => {}
833 1 => tokio::time::sleep(Duration::from_secs(11)).await,
834 _ => std::future::pending::<()>().await,
835 }
836 }
837 }));
838
839 assert_eq!(rx.recv().await, Some(0), "the first run returns immediately");
840 tokio::time::advance(Duration::from_millis(1)).await;
841 assert_eq!(rx.recv().await, Some(1), "the second run starts");
842 tokio::time::advance(Duration::from_secs(11)).await;
843 tokio::time::advance(Duration::from_millis(1)).await;
844 assert_eq!(rx.recv().await, Some(2), "the third run starts");
845
846 assert_eq!(
847 member(&supervisor, "probe").restarts,
848 1,
849 "a run that outlived reset_after starts the count over",
850 );
851 }
852}