1mod 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#[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 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 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 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 #[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}