Skip to main content

irtt_client/managed/
task.rs

1//! Task ownership, durable status/history, and top-level polling and sealing.
2//!
3//! Private children implement control, target lifecycle, updates, pacing, and
4//! bounded timeout work on this same task; they do not introduce other owners.
5
6mod control;
7mod pacing;
8mod target;
9mod timeouts;
10mod update;
11
12pub use control::{ManagedClient, ManagedClientHandle, ManagedCommandReceipt, ManagedStopReceipt};
13
14use std::{
15    collections::VecDeque,
16    future::Future,
17    pin::Pin,
18    sync::Arc,
19    task::{Context, Poll},
20    time::Instant,
21};
22
23use tokio::{
24    sync::{broadcast, mpsc, watch},
25    time::{self, Sleep},
26};
27
28use super::{
29    ManagedClientConfig, ManagedCommandApplyError, ManagedCompletionPolicy, ManagedDriverFailure,
30    ManagedEndReason, ManagedEvent, ManagedLifecycle, ManagedOutcome, ManagedPacing, ManagedStatus,
31    ManagedTargetEndReason, ManagedTargetLifecycle, ManagedTargetOutcome, ManagedTargetStatus,
32};
33use control::{ManagedCommand, StopSignal};
34use pacing::gated_send_deadline;
35use target::{TargetPhase, TargetRuntime};
36use timeouts::TimeoutBacklogEntry;
37
38const TARGET_WORK_BUDGET: usize = 128;
39
40type WakeFuture = Pin<Box<dyn Future<Output = Option<watch::Receiver<()>>> + Send + 'static>>;
41
42fn arm_wake(mut receiver: watch::Receiver<()>) -> WakeFuture {
43    Box::pin(async move {
44        receiver.changed().await.ok()?;
45        Some(receiver)
46    })
47}
48
49#[derive(Default)]
50struct OutcomeHistory {
51    limit: usize,
52    recent: VecDeque<ManagedTargetOutcome>,
53    total: u64,
54    successful: u64,
55    failed: u64,
56    peer_closed: u64,
57    discarded: u64,
58}
59
60impl OutcomeHistory {
61    fn new(limit: usize) -> Self {
62        Self {
63            limit,
64            ..Self::default()
65        }
66    }
67
68    fn record(&mut self, outcome: ManagedTargetOutcome) {
69        self.total = self.total.saturating_add(1);
70        match &outcome.end_reason {
71            ManagedTargetEndReason::Failed(_) => self.failed = self.failed.saturating_add(1),
72            ManagedTargetEndReason::PeerClosed => {
73                self.successful = self.successful.saturating_add(1);
74                self.peer_closed = self.peer_closed.saturating_add(1);
75            }
76            ManagedTargetEndReason::TestComplete
77            | ManagedTargetEndReason::NoTestComplete
78            | ManagedTargetEndReason::Removed
79            | ManagedTargetEndReason::Replaced
80            | ManagedTargetEndReason::Stopped => {
81                self.successful = self.successful.saturating_add(1);
82            }
83        }
84        if self.limit == 0 {
85            self.discarded = self.discarded.saturating_add(1);
86            return;
87        }
88        if self.recent.len() == self.limit {
89            self.recent.pop_front();
90            self.discarded = self.discarded.saturating_add(1);
91        }
92        self.recent.push_back(outcome);
93    }
94
95    fn recent(&self) -> Arc<[ManagedTargetOutcome]> {
96        Arc::from(
97            self.recent
98                .iter()
99                .cloned()
100                .collect::<Vec<_>>()
101                .into_boxed_slice(),
102        )
103    }
104
105    fn outcome(
106        &self,
107        end_reason: ManagedEndReason,
108        applied_command_sequence: u64,
109    ) -> ManagedOutcome {
110        ManagedOutcome {
111            end_reason,
112            applied_command_sequence,
113            total_target_outcomes: self.total,
114            successful_target_outcomes: self.successful,
115            failed_target_outcomes: self.failed,
116            peer_closed_target_outcomes: self.peer_closed,
117            discarded_target_outcomes: self.discarded,
118            recent_target_outcomes: self.recent(),
119        }
120    }
121}
122
123struct TaskResources {
124    status: watch::Sender<Arc<ManagedStatus>>,
125    events: Option<broadcast::Sender<ManagedEvent>>,
126    stop: Arc<StopSignal>,
127}
128
129enum TaskPhase {
130    NotStarted,
131    Running,
132    Stopping,
133    Completed { outcome: Arc<ManagedOutcome> },
134    Failed { outcome: Arc<ManagedOutcome> },
135    Abandoned,
136}
137
138impl TaskPhase {
139    fn lifecycle(&self) -> ManagedLifecycle {
140        match self {
141            Self::NotStarted => ManagedLifecycle::NotStarted,
142            Self::Running => ManagedLifecycle::Running,
143            Self::Stopping => ManagedLifecycle::Stopping,
144            Self::Completed { .. } => ManagedLifecycle::Completed,
145            Self::Failed { .. } => ManagedLifecycle::Failed,
146            Self::Abandoned => ManagedLifecycle::Abandoned,
147        }
148    }
149
150    fn final_outcome(&self) -> Option<Arc<ManagedOutcome>> {
151        match self {
152            Self::Completed { outcome } | Self::Failed { outcome } => Some(Arc::clone(outcome)),
153            _ => None,
154        }
155    }
156
157    fn is_terminal(&self) -> bool {
158        matches!(
159            self,
160            Self::Completed { .. } | Self::Failed { .. } | Self::Abandoned
161        )
162    }
163
164    fn is_running(&self) -> bool {
165        matches!(self, Self::Running)
166    }
167
168    fn is_stopping(&self) -> bool {
169        matches!(self, Self::Stopping)
170    }
171}
172
173/// The sole authoritative zero-or-many target driver.
174#[must_use = "ManagedClientTask must be awaited or deliberately dropped"]
175pub struct ManagedClientTask {
176    #[cfg(test)]
177    timeout_inspections: std::cell::RefCell<Vec<usize>>,
178    phase: TaskPhase,
179    config: ManagedClientConfig,
180    targets: Vec<TargetRuntime>,
181    commands: mpsc::Receiver<ManagedCommand>,
182    history: OutcomeHistory,
183    resources: Option<TaskResources>,
184    wake: Option<WakeFuture>,
185    timer: Option<Pin<Box<Sleep>>>,
186    cursor: usize,
187    timeout_cursor: usize,
188    timeout_backlog: VecDeque<TimeoutBacklogEntry>,
189    scan_remaining: usize,
190    send_cursor: usize,
191    burst_remaining: usize,
192    stagger_remaining: usize,
193    send_gate: Option<Instant>,
194    last_stagger_send: Option<Instant>,
195    // Explicit stop observation is independent of natural stopping or terminality.
196    stop_observed: bool,
197    next_generation: u64,
198    applied_command_sequence: u64,
199}
200
201impl ManagedClientTask {
202    fn resources(&self) -> &TaskResources {
203        self.resources
204            .as_ref()
205            .expect("managed task resources exist before terminal sealing")
206    }
207
208    fn publish_event(&self, event: ManagedEvent) {
209        if let Some(events) = &self.resources().events {
210            let _ = events.send(event);
211        }
212    }
213
214    fn snapshot(&self) -> Arc<ManagedStatus> {
215        let mut connecting = 0;
216        let mut opening = 0;
217        let mut active = 0;
218        let mut draining = 0;
219        let mut closing = 0;
220        let mut terminal = 0;
221        let targets = self
222            .targets
223            .iter()
224            .map(|target| {
225                let lifecycle = target.phase().lifecycle();
226                match lifecycle {
227                    ManagedTargetLifecycle::Pending => {}
228                    ManagedTargetLifecycle::Connecting => connecting += 1,
229                    ManagedTargetLifecycle::Opening => opening += 1,
230                    ManagedTargetLifecycle::Active => active += 1,
231                    ManagedTargetLifecycle::Draining => draining += 1,
232                    ManagedTargetLifecycle::Closing => closing += 1,
233                    ManagedTargetLifecycle::Terminal => terminal += 1,
234                }
235                ManagedTargetStatus {
236                    target: target.instance.clone(),
237                    desired: target.membership.is_desired(),
238                    lifecycle,
239                    outcome: match target.phase() {
240                        TargetPhase::Terminal { outcome } => Some(Arc::clone(outcome)),
241                        _ => None,
242                    },
243                    server_addr: Arc::clone(&target.server_addr),
244                    remote: target.remote,
245                }
246            })
247            .collect::<Vec<_>>();
248        Arc::new(ManagedStatus {
249            lifecycle: self.phase.lifecycle(),
250            stop_requested: self.stop_observed,
251            applied_command_sequence: self.applied_command_sequence,
252            desired_target_count: self
253                .targets
254                .iter()
255                .filter(|target| target.membership.is_desired())
256                .count(),
257            connecting_target_count: connecting,
258            opening_target_count: opening,
259            active_target_count: active,
260            draining_target_count: draining,
261            closing_target_count: closing,
262            terminal_target_count: terminal,
263            total_target_outcomes: self.history.total,
264            successful_target_outcomes: self.history.successful,
265            failed_target_outcomes: self.history.failed,
266            peer_closed_target_outcomes: self.history.peer_closed,
267            discarded_target_outcomes: self.history.discarded,
268            targets: Arc::from(targets.into_boxed_slice()),
269            recent_target_outcomes: self.history.recent(),
270            final_outcome: self.phase.final_outcome(),
271        })
272    }
273
274    fn replace_status(&self) {
275        self.resources().status.send_replace(self.snapshot());
276    }
277
278    fn begin_running(&mut self) -> Result<(), ManagedDriverFailure> {
279        tokio::runtime::Handle::try_current().map_err(|_| ManagedDriverFailure::NoTokioRuntime)?;
280        self.phase = TaskPhase::Running;
281        self.replace_status();
282        self.publish_event(ManagedEvent::Started);
283        Ok(())
284    }
285
286    fn observe_stop(&mut self) {
287        if self.stop_observed {
288            return;
289        }
290        self.stop_observed = true;
291        self.resources().stop.begin_stopping();
292        let lifecycle_transition = !self.phase.is_stopping();
293        self.phase = TaskPhase::Stopping;
294        self.replace_status();
295        if lifecycle_transition {
296            self.publish_event(ManagedEvent::Stopping);
297        }
298        self.scan_remaining = self.targets.len();
299        self.burst_remaining = 0;
300    }
301
302    fn poll_target_pass(&mut self, cx: &mut Context<'_>, now: Instant) -> bool {
303        if self.targets.is_empty() {
304            return false;
305        }
306        if self.scan_remaining == 0 {
307            self.scan_remaining = self.targets.len();
308        }
309        let work = self.scan_remaining.min(TARGET_WORK_BUDGET);
310        let mut immediate = false;
311        for _ in 0..work {
312            let index = self.cursor % self.targets.len();
313            self.cursor = (self.cursor + 1) % self.targets.len();
314            self.scan_remaining -= 1;
315            immediate |= self.poll_target(index, cx, now);
316        }
317        immediate || self.scan_remaining > 0
318    }
319
320    fn all_targets_terminal(&self) -> bool {
321        self.targets
322            .iter()
323            .all(|target| matches!(target.phase(), TargetPhase::Terminal { .. }))
324    }
325
326    fn next_deadline(&self) -> Option<Instant> {
327        let mut non_send_deadline = None;
328        let mut send_deadline = None;
329        for target in &self.targets {
330            match target.phase() {
331                TargetPhase::Active {
332                    client,
333                    schedule,
334                    send_waiting,
335                } => {
336                    non_send_deadline = non_send_deadline
337                        .into_iter()
338                        .chain(client.next_probe_timeout_deadline())
339                        .chain(schedule.end_deadline())
340                        .min();
341                    if target.membership.is_desired() && !send_waiting {
342                        send_deadline = send_deadline
343                            .into_iter()
344                            .chain(schedule.next_send_deadline())
345                            .min();
346                    }
347                }
348                TargetPhase::Draining {
349                    client, deadline, ..
350                } => {
351                    non_send_deadline = non_send_deadline
352                        .into_iter()
353                        .chain(client.next_probe_timeout_deadline())
354                        .chain(Some(*deadline))
355                        .min();
356                }
357                TargetPhase::Closing { deadline, .. } => {
358                    non_send_deadline = non_send_deadline.into_iter().chain(Some(*deadline)).min();
359                }
360                _ => {}
361            }
362        }
363        if self.phase.is_running()
364            && self.config.pacing == ManagedPacing::Staggered
365            && self.active_count() > 0
366        {
367            send_deadline = gated_send_deadline(send_deadline, self.send_gate);
368        }
369        non_send_deadline.into_iter().chain(send_deadline).min()
370    }
371
372    fn register_timer(&mut self, cx: &mut Context<'_>) -> bool {
373        let Some(deadline) = self.next_deadline() else {
374            self.timer = None;
375            return false;
376        };
377        let timer = self
378            .timer
379            .get_or_insert_with(|| Box::pin(time::sleep_until(deadline.into())));
380        timer.as_mut().reset(deadline.into());
381        timer.as_mut().poll(cx).is_ready()
382    }
383
384    fn register_stop_wake(&mut self, cx: &mut Context<'_>) -> bool {
385        let Some(mut wake) = self.wake.take() else {
386            return false;
387        };
388        match wake.as_mut().poll(cx) {
389            Poll::Pending => {
390                self.wake = Some(wake);
391                false
392            }
393            Poll::Ready(Some(receiver)) => {
394                self.wake = Some(arm_wake(receiver));
395                self.resources().stop.is_requested()
396            }
397            Poll::Ready(None) => false,
398        }
399    }
400
401    fn seal(&mut self, mut end_reason: ManagedEndReason, failed: bool) -> Poll<ManagedOutcome> {
402        // Sealing is the final stop-observation point.  A stop that reaches the
403        // latch before this terminal linearization becomes durable even when
404        // quiescence had already entered `Stopping`.
405        if self.resources().stop.is_requested() {
406            self.observe_stop();
407        }
408        if self.stop_observed && matches!(end_reason, ManagedEndReason::TargetsComplete) {
409            end_reason = ManagedEndReason::StopRequested;
410        }
411
412        // Closing admission before closing and draining the receiver makes every
413        // accepted command task-owned.  Tokio still permits buffered commands
414        // to be drained after `Receiver::close`, but rejects later sends.
415        self.resources().stop.close_updates();
416        self.commands.close();
417        while let Ok(ManagedCommand::UpdateTargets {
418            acknowledgement, ..
419        }) = self.commands.try_recv()
420        {
421            let error = match &end_reason {
422                ManagedEndReason::DriverFailed(failure) => {
423                    ManagedCommandApplyError::DriverFailed(failure.clone())
424                }
425                _ => ManagedCommandApplyError::Stopping,
426            };
427            let _ = acknowledgement.send(Err(error));
428        }
429        let outcome = Arc::new(
430            self.history
431                .outcome(end_reason, self.applied_command_sequence),
432        );
433        self.phase = if failed {
434            TaskPhase::Failed {
435                outcome: Arc::clone(&outcome),
436            }
437        } else {
438            TaskPhase::Completed {
439                outcome: Arc::clone(&outcome),
440            }
441        };
442        self.replace_status();
443        self.publish_event(if failed {
444            ManagedEvent::Failed {
445                outcome: Arc::clone(&outcome),
446            }
447        } else {
448            ManagedEvent::Completed {
449                outcome: Arc::clone(&outcome),
450            }
451        });
452        if let Some(resources) = self.resources.as_mut() {
453            resources.events.take();
454        }
455        self.wake = None;
456        self.timer = None;
457        self.resources.take();
458        Poll::Ready((*outcome).clone())
459    }
460
461    fn fail_driver(&mut self, failure: ManagedDriverFailure) -> Poll<ManagedOutcome> {
462        self.seal(ManagedEndReason::DriverFailed(failure), true)
463    }
464}
465
466impl Future for ManagedClientTask {
467    type Output = ManagedOutcome;
468
469    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
470        let this = self.as_mut().get_mut();
471        if this.phase.is_terminal() {
472            panic!("ManagedClientTask polled after completion");
473        }
474
475        if matches!(this.phase, TaskPhase::NotStarted) {
476            if this.resources().stop.is_requested() {
477                this.observe_stop();
478            } else if let Err(failure) = this.begin_running() {
479                return this.fail_driver(failure);
480            }
481        }
482        if this.resources().stop.is_requested() {
483            this.observe_stop();
484        }
485
486        let mut immediate = this.process_commands(cx);
487        if this.resources().stop.is_requested() {
488            this.observe_stop();
489        }
490        let now = Instant::now();
491        immediate |= this.poll_timeout_pass(now);
492        immediate |= this.poll_target_pass(cx, now);
493        this.prune_undesired_terminal();
494        if this.phase.is_running() {
495            immediate |= match this.config.pacing {
496                ManagedPacing::Staggered => this.poll_staggered_send(cx, now),
497                ManagedPacing::Burst => this.poll_burst_sends(cx, now),
498            };
499        }
500
501        if this.resources().stop.is_requested() {
502            this.observe_stop();
503        }
504
505        if this.all_targets_terminal() {
506            match this.phase {
507                TaskPhase::Stopping => {
508                    return this.seal(
509                        if this.stop_observed {
510                            ManagedEndReason::StopRequested
511                        } else {
512                            ManagedEndReason::TargetsComplete
513                        },
514                        false,
515                    );
516                }
517                TaskPhase::Running
518                    if this.config.completion == ManagedCompletionPolicy::FinishWhenQuiescent =>
519                {
520                    this.phase = TaskPhase::Stopping;
521                    this.resources().stop.begin_stopping();
522                    this.replace_status();
523                    this.publish_event(ManagedEvent::Stopping);
524                    return this.seal(ManagedEndReason::TargetsComplete, false);
525                }
526                _ => {}
527            }
528        }
529
530        immediate |= this.register_stop_wake(cx);
531        if !immediate {
532            immediate |= this.register_timer(cx);
533        }
534        if immediate {
535            cx.waker().wake_by_ref();
536        }
537        Poll::Pending
538    }
539}
540
541impl Drop for ManagedClientTask {
542    fn drop(&mut self) {
543        if self.phase.is_terminal() || self.resources.is_none() {
544            return;
545        }
546        self.phase = TaskPhase::Abandoned;
547        self.resources().stop.close_updates();
548        self.commands.close();
549        self.replace_status();
550        self.publish_event(ManagedEvent::Abandoned);
551        if let Some(resources) = self.resources.as_mut() {
552            resources.events.take();
553        }
554        self.resources.take();
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::{
562        async_client::AsyncClient,
563        managed::{schedule::ProbeSchedule, ManagedTargetConfig},
564        ClientConfig,
565    };
566    use std::time::Duration;
567
568    // Real EAGAIN needs privileged queue shaping. Inspect only the owning
569    // deadline selection here, using a production socket without sending.
570    #[test]
571    fn finite_lifetime_deadline_survives_send_waiting_and_stagger_gating() {
572        let runtime = tokio::runtime::Builder::new_current_thread()
573            .enable_all()
574            .build()
575            .unwrap();
576        runtime.block_on(async {
577            let start = Instant::now();
578            let end = start + Duration::from_millis(500);
579            let accepted = crate::AcceptedSessionParameters {
580                duration: Some(Duration::from_millis(500)),
581                interval: Duration::from_millis(1),
582                length: 0,
583                received_stats: irtt_proto::ReceivedStats::None,
584                stamp_at: irtt_proto::StampAt::None,
585                clock: irtt_proto::Clock::Both,
586                dscp: 0,
587                server_fill: None,
588            };
589            for pacing in [ManagedPacing::Burst, ManagedPacing::Staggered] {
590                let (mut task, _handle) = ManagedClient::task(
591                    ManagedClientConfig {
592                        pacing,
593                        ..ManagedClientConfig::default()
594                    },
595                    vec![ManagedTargetConfig::new("target", "127.0.0.1:9")],
596                )
597                .unwrap();
598                let client = AsyncClient::connect("127.0.0.1:9", ClientConfig::default())
599                    .await
600                    .unwrap();
601                task.phase = TaskPhase::Running;
602                task.install_target_phase(
603                    0,
604                    TargetPhase::Active {
605                        client,
606                        schedule: ProbeSchedule::new(start, &accepted).unwrap(),
607                        send_waiting: false,
608                    },
609                );
610                task.send_gate = Some(end + Duration::from_secs(1));
611
612                for send_waiting in [false, true] {
613                    let TargetPhase::Active {
614                        schedule,
615                        send_waiting: waiting,
616                        ..
617                    } = task.targets[0].phase_mut()
618                    else {
619                        unreachable!()
620                    };
621                    *waiting = send_waiting;
622                    *schedule = ProbeSchedule::new(start, &accepted).unwrap();
623                    let expected = if pacing == ManagedPacing::Burst && !send_waiting {
624                        start
625                    } else {
626                        end
627                    };
628                    assert_eq!(task.next_deadline(), Some(expected));
629
630                    let TargetPhase::Active { schedule, .. } = task.targets[0].phase_mut() else {
631                        unreachable!()
632                    };
633                    schedule.permit_probe_at(end);
634                    assert_eq!(task.next_deadline(), None);
635                }
636
637                let mut continuous = accepted.clone();
638                continuous.duration = None;
639                let TargetPhase::Active { schedule, .. } = task.targets[0].phase_mut() else {
640                    unreachable!()
641                };
642                *schedule = ProbeSchedule::new(start, &continuous).unwrap();
643                assert_eq!(task.next_deadline(), None);
644            }
645        });
646    }
647}