Skip to main content

irtt_client/managed/
runner.rs

1use std::{
2    thread::{self, JoinHandle},
3    time::{Duration, Instant},
4};
5
6use crate::{
7    config::{ClientConfig, RecvBudget},
8    error::ClientError,
9    event::{ClientEvent, OpenOutcome},
10    Client,
11};
12
13use super::{
14    cancellation::CancellationToken,
15    hub::{EventHub, EventSubscription, SubscriberConfig},
16};
17
18const MANAGED_RECV_TIMEOUT: Duration = Duration::from_millis(20);
19const MANAGED_RECV_BUDGET: RecvBudget = RecvBudget { max_packets: 64 };
20const MANAGED_FINAL_DRAIN: Duration = Duration::from_millis(100);
21const IDLE_SLEEP: Duration = Duration::from_millis(1);
22const MAX_SLEEP: Duration = Duration::from_millis(20);
23
24/// Entry point for running a client session on a worker thread.
25///
26/// The managed API owns the lower-level [`Client`](crate::Client) loop: it
27/// opens the session, sends probes at the negotiated interval, receives
28/// datagrams, publishes [`ClientEvent`] values, and closes the session when the
29/// run completes or is cancelled.
30#[derive(Debug)]
31pub struct ManagedClient;
32
33/// Running managed client session.
34///
35/// [`ManagedClientSession::join`] waits for the worker and returns the session
36/// outcome or client error. Dropping the session requests cooperative
37/// cancellation; callers that need the outcome should explicitly join instead
38/// of relying on drop.
39#[must_use = "dropping the session cancels the managed client; call join() to wait for completion"]
40#[derive(Debug)]
41pub struct ManagedClientSession {
42    hub: EventHub,
43    cancellation: CancellationToken,
44    worker: Option<JoinHandle<Result<SessionOutcome, ClientError>>>,
45}
46
47/// Outcome returned by a completed managed client session.
48///
49/// These are runner-level lifecycle counters, not statistical summaries. Use
50/// `irtt-stats` with emitted `ClientEvent` values for RTT, loss, IPDV, and
51/// related summaries.
52#[must_use = "managed session outcomes contain completion status and counters"]
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SessionOutcome {
55    /// Why the managed loop stopped.
56    pub end_reason: SessionEndReason,
57    /// Number of echo requests sent by the client.
58    pub packets_sent: u64,
59    /// Number of first in-window echo replies received.
60    pub replies_received: u64,
61    /// Number of duplicate reply events emitted.
62    pub duplicates: u64,
63    /// Number of late reply events emitted.
64    pub late: u64,
65    /// Number of warning events emitted.
66    pub warning_events: u64,
67}
68
69/// Reason a managed client session ended.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SessionEndReason {
72    /// The negotiated finite test duration completed and the client closed the
73    /// session normally.
74    TestComplete,
75    /// Cancellation was requested through [`ManagedClientSession::stop`] or by
76    /// dropping the session handle.
77    Cancelled,
78    /// The session was opened in [`RunMode::NoTest`](crate::RunMode) and
79    /// completed after negotiation.
80    NoTestComplete,
81}
82
83impl ManagedClient {
84    /// Start a managed session without creating an initial event subscription.
85    ///
86    /// The returned [`ManagedClientSession`] can still create subscriptions via
87    /// [`ManagedClientSession::subscribe`], but events emitted before a
88    /// subscriber is registered are not replayed.
89    pub fn start(config: ClientConfig) -> Result<ManagedClientSession, ClientError> {
90        Self::start_inner(config, None).map(|(session, _)| session)
91    }
92
93    /// Start a managed session and subscribe to events before the worker runs.
94    ///
95    /// The initial subscription receives the open lifecycle event and
96    /// subsequent session events, subject to its queue capacity and overflow
97    /// policy.
98    pub fn start_with_subscription(
99        config: ClientConfig,
100        subscriber_config: SubscriberConfig,
101    ) -> Result<(ManagedClientSession, EventSubscription), ClientError> {
102        let (session, subscription) = Self::start_inner(config, Some(subscriber_config))?;
103        Ok((
104            session,
105            subscription.expect("initial subscription must be present"),
106        ))
107    }
108
109    fn start_inner(
110        mut config: ClientConfig,
111        subscriber_config: Option<SubscriberConfig>,
112    ) -> Result<(ManagedClientSession, Option<EventSubscription>), ClientError> {
113        if config.socket_config.recv_timeout.is_none()
114            || config.socket_config.recv_timeout > Some(MANAGED_RECV_TIMEOUT)
115        {
116            config.socket_config.recv_timeout = Some(MANAGED_RECV_TIMEOUT);
117        }
118
119        let hub = EventHub::new();
120        let initial_subscription = subscriber_config
121            .map(|config| hub.subscribe(config))
122            .transpose()?;
123
124        let mut client = Client::connect(config)?;
125        let outcome = client.open()?;
126        publish_open_outcome(&hub, &outcome);
127
128        let cancellation = CancellationToken::new();
129        let worker_hub = hub.clone();
130        let worker_cancellation = cancellation.clone();
131        let worker =
132            thread::spawn(move || run_client_with_cleanup(client, worker_hub, worker_cancellation));
133
134        Ok((
135            ManagedClientSession {
136                hub,
137                cancellation,
138                worker: Some(worker),
139            },
140            initial_subscription,
141        ))
142    }
143}
144
145impl ManagedClientSession {
146    /// Add another event subscriber to this running managed session.
147    ///
148    /// The subscription receives only events published after it is registered.
149    pub fn subscribe(&self, config: SubscriberConfig) -> Result<EventSubscription, ClientError> {
150        self.hub.subscribe(config)
151    }
152
153    /// Request cooperative cancellation of the managed session.
154    ///
155    /// Cancellation is observed by the worker loop between socket receive,
156    /// timeout polling, and send scheduling steps. Call [`join`](Self::join) to
157    /// wait for the worker and obtain the final [`SessionOutcome`].
158    pub fn stop(&self) {
159        self.cancellation.cancel();
160    }
161
162    /// Wait for the managed worker thread to finish.
163    ///
164    /// On success, returns lifecycle counters for the completed session. If the
165    /// worker panicked, this returns [`ClientError::WorkerPanicked`]. Joining
166    /// also disconnects all event subscriptions while leaving already queued
167    /// events available to drain.
168    pub fn join(mut self) -> Result<SessionOutcome, ClientError> {
169        let worker = self
170            .worker
171            .take()
172            .expect("ManagedClientSession invariant violated: worker handle missing before join");
173        match worker.join() {
174            Ok(outcome) => {
175                self.hub.disconnect_all();
176                outcome
177            }
178            Err(_) => {
179                self.hub.disconnect_all();
180                Err(ClientError::WorkerPanicked)
181            }
182        }
183    }
184}
185
186impl Drop for ManagedClientSession {
187    fn drop(&mut self) {
188        self.cancellation.cancel();
189    }
190}
191
192fn publish_open_outcome(hub: &EventHub, outcome: &OpenOutcome) {
193    match outcome {
194        OpenOutcome::Started { event, .. } | OpenOutcome::NoTestCompleted { event, .. } => {
195            hub.publish(event.clone());
196        }
197    }
198}
199
200fn run_client(
201    mut client: Client,
202    hub: EventHub,
203    cancellation: CancellationToken,
204) -> Result<SessionOutcome, ClientError> {
205    if client.is_run_complete() {
206        return Ok(SessionOutcome {
207            end_reason: SessionEndReason::NoTestComplete,
208            packets_sent: 0,
209            replies_received: 0,
210            duplicates: 0,
211            late: 0,
212            warning_events: 0,
213        });
214    }
215
216    let mut counters = OutcomeCounters::default();
217    let mut cancelled = false;
218
219    loop {
220        if cancellation.is_cancelled() {
221            cancelled = true;
222            publish_events(
223                &hub,
224                &mut counters,
225                client.recv_available(MANAGED_RECV_BUDGET)?,
226            );
227            publish_events(&hub, &mut counters, client.poll_timeouts()?);
228            break;
229        }
230
231        let now = Instant::now();
232        if client
233            .next_send_deadline()
234            .is_some_and(|deadline| deadline <= now)
235        {
236            let events = client.send_probe()?;
237            publish_events(&hub, &mut counters, events);
238            continue;
239        }
240
241        publish_events(
242            &hub,
243            &mut counters,
244            client.recv_available(MANAGED_RECV_BUDGET)?,
245        );
246        publish_events(&hub, &mut counters, client.poll_timeouts()?);
247
248        if client.is_run_complete() {
249            break;
250        }
251
252        sleep_until_next_wakeup(client.next_send_deadline());
253    }
254
255    if !cancelled {
256        drain_final_late_replies(&mut client, &hub, &mut counters)?;
257    }
258
259    let packets_sent = client.packets_sent();
260    let close_events = client.close()?;
261    publish_events(&hub, &mut counters, close_events);
262
263    Ok(SessionOutcome {
264        end_reason: if cancelled {
265            SessionEndReason::Cancelled
266        } else {
267            SessionEndReason::TestComplete
268        },
269        packets_sent,
270        replies_received: counters.replies_received,
271        duplicates: counters.duplicates,
272        late: counters.late,
273        warning_events: counters.warning_events,
274    })
275}
276
277fn run_client_with_cleanup(
278    client: Client,
279    hub: EventHub,
280    cancellation: CancellationToken,
281) -> Result<SessionOutcome, ClientError> {
282    let outcome = run_client(client, hub.clone(), cancellation);
283    hub.disconnect_all();
284    outcome
285}
286
287fn drain_final_late_replies(
288    client: &mut Client,
289    hub: &EventHub,
290    counters: &mut OutcomeCounters,
291) -> Result<(), ClientError> {
292    if !client.has_timed_out_metadata() {
293        return Ok(());
294    }
295
296    let deadline = Instant::now() + MANAGED_FINAL_DRAIN;
297    while Instant::now() < deadline && client.has_timed_out_metadata() {
298        let mut published = false;
299
300        let events = client.recv_available(MANAGED_RECV_BUDGET)?;
301        published |= !events.is_empty();
302        publish_events(hub, counters, events);
303
304        let events = client.poll_timeouts()?;
305        published |= !events.is_empty();
306        publish_events(hub, counters, events);
307
308        if !published {
309            thread::sleep(IDLE_SLEEP);
310        }
311    }
312    Ok(())
313}
314
315fn publish_events(hub: &EventHub, counters: &mut OutcomeCounters, events: Vec<ClientEvent>) {
316    for event in events {
317        counters.observe(&event);
318        hub.publish(event);
319    }
320}
321
322fn sleep_until_next_wakeup(deadline: Option<Instant>) {
323    let sleep_for = deadline
324        .and_then(|deadline| deadline.checked_duration_since(Instant::now()))
325        .map(|duration| duration.min(MAX_SLEEP))
326        .unwrap_or(IDLE_SLEEP);
327    if sleep_for > Duration::ZERO {
328        thread::sleep(sleep_for);
329    }
330}
331
332#[derive(Debug, Default)]
333struct OutcomeCounters {
334    replies_received: u64,
335    duplicates: u64,
336    late: u64,
337    warning_events: u64,
338}
339
340impl OutcomeCounters {
341    fn observe(&mut self, event: &ClientEvent) {
342        match event {
343            ClientEvent::EchoReply { .. } => self.replies_received += 1,
344            ClientEvent::DuplicateReply { .. } => self.duplicates += 1,
345            ClientEvent::LateReply { .. } => self.late += 1,
346            ClientEvent::Warning { .. } => self.warning_events += 1,
347            _ => {}
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::{config::NegotiationPolicy, SubscriberOverflow};
356    use irtt_proto::{
357        echo_packet_len,
358        flags::{self, FLAG_OPEN, FLAG_REPLY},
359        layout::PacketLayout,
360        Clock, Params, ReceivedStats, StampAt, TimestampFields, MAGIC, PROTOCOL_VERSION,
361    };
362    use std::{
363        net::{SocketAddr, UdpSocket},
364        sync::mpsc,
365    };
366
367    const TOKEN: u64 = 0x1234_5678_90ab_cdef;
368    struct FakeServer {
369        addr: SocketAddr,
370        done: JoinHandle<()>,
371    }
372
373    impl FakeServer {
374        fn join(self) {
375            self.done.join().unwrap();
376        }
377    }
378
379    fn test_echo_packet_len(hmac: bool, params: &Params) -> usize {
380        echo_packet_len(hmac, params)
381            .expect("managed runner test params must have a non-negative packet length")
382    }
383
384    fn test_params(duration: Option<Duration>, interval: Duration) -> Params {
385        Params {
386            protocol_version: PROTOCOL_VERSION,
387            duration_ns: duration.map_or(0, test_duration_ns_i64),
388            interval_ns: test_duration_ns_i64(interval),
389            length: 0,
390            received_stats: ReceivedStats::Both,
391            stamp_at: StampAt::Both,
392            clock: Clock::Both,
393            dscp: 0,
394            server_fill: None,
395        }
396    }
397
398    fn test_duration_ns_i64(duration: Duration) -> i64 {
399        i64::try_from(duration.as_nanos()).expect("test duration fits i64 nanoseconds")
400    }
401
402    fn config(addr: SocketAddr, duration: Option<Duration>) -> ClientConfig {
403        ClientConfig {
404            server_addr: addr.to_string(),
405            duration,
406            interval: Duration::from_millis(10),
407            negotiation_policy: NegotiationPolicy::Strict,
408            open_timeouts: vec![Duration::from_millis(200)],
409            probe_timeout: Duration::from_millis(40),
410            ..ClientConfig::default()
411        }
412    }
413
414    fn start_echo_server(params: Params) -> FakeServer {
415        let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
416        let addr = socket.local_addr().unwrap();
417        let done = thread::spawn(move || {
418            let (_, peer) = recv_request(&socket);
419            socket
420                .send_to(&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &params), peer)
421                .unwrap();
422            socket
423                .set_read_timeout(Some(Duration::from_millis(500)))
424                .unwrap();
425
426            while let Some((packet, peer)) = recv_request_timeout(&socket) {
427                if packet[3] & flags::FLAG_CLOSE != 0 {
428                    break;
429                }
430
431                let seq = u32::from_le_bytes(packet[12..16].try_into().unwrap());
432                let ts = TimestampFields {
433                    recv_wall: Some(1_000_000_000),
434                    recv_mono: Some(100_000),
435                    send_wall: Some(1_000_000_000),
436                    send_mono: Some(100_000),
437                    ..Default::default()
438                };
439                socket
440                    .send_to(&echo_reply_packet(TOKEN, seq, &params, &ts), peer)
441                    .unwrap();
442            }
443        });
444        FakeServer { addr, done }
445    }
446
447    fn start_delayed_reply_server(params: Params, delay: Duration) -> FakeServer {
448        let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
449        let addr = socket.local_addr().unwrap();
450        let done = thread::spawn(move || {
451            let (_, peer) = recv_request(&socket);
452            socket
453                .send_to(&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &params), peer)
454                .unwrap();
455            socket
456                .set_read_timeout(Some(Duration::from_millis(500)))
457                .unwrap();
458
459            let Some((packet, peer)) = recv_request_timeout(&socket) else {
460                return;
461            };
462            let seq = u32::from_le_bytes(packet[12..16].try_into().unwrap());
463            thread::sleep(delay);
464            let ts = TimestampFields {
465                recv_wall: Some(1_000_000_000),
466                recv_mono: Some(100_000),
467                send_wall: Some(1_001_000_000),
468                send_mono: Some(1_100_000),
469                ..Default::default()
470            };
471            socket
472                .send_to(&echo_reply_packet(TOKEN, seq, &params, &ts), peer)
473                .unwrap();
474
475            while let Some((packet, _)) = recv_request_timeout(&socket) {
476                if packet[3] & flags::FLAG_CLOSE != 0 {
477                    break;
478                }
479            }
480        });
481        FakeServer { addr, done }
482    }
483
484    fn recv_request(socket: &UdpSocket) -> (Vec<u8>, SocketAddr) {
485        let mut buf = [0_u8; 2048];
486        let (size, peer) = socket.recv_from(&mut buf).unwrap();
487        (buf[..size].to_vec(), peer)
488    }
489
490    fn recv_request_timeout(socket: &UdpSocket) -> Option<(Vec<u8>, SocketAddr)> {
491        let mut buf = [0_u8; 2048];
492        socket
493            .recv_from(&mut buf)
494            .ok()
495            .map(|(size, peer)| (buf[..size].to_vec(), peer))
496    }
497
498    fn open_reply(flags: u8, token: u64, params: &Params) -> Vec<u8> {
499        let mut packet = Vec::new();
500        packet.extend_from_slice(&MAGIC);
501        packet.push(flags);
502        packet.extend_from_slice(&token.to_le_bytes());
503        packet.extend_from_slice(&params.encode());
504        packet
505    }
506
507    fn echo_reply_packet(
508        token: u64,
509        seq: u32,
510        params: &Params,
511        timestamps: &TimestampFields,
512    ) -> Vec<u8> {
513        let layout = PacketLayout::echo(false, params);
514        let packet_len = test_echo_packet_len(false, params);
515        let mut packet = Vec::with_capacity(packet_len);
516
517        packet.extend_from_slice(&MAGIC);
518        packet.push(FLAG_REPLY);
519        packet.extend_from_slice(&token.to_le_bytes());
520        packet.extend_from_slice(&seq.to_le_bytes());
521
522        if layout.recv_count {
523            packet.extend_from_slice(&42_u32.to_le_bytes());
524        }
525        if layout.recv_window {
526            packet.extend_from_slice(&0x07_u64.to_le_bytes());
527        }
528        if layout.recv_wall {
529            packet.extend_from_slice(&timestamps.recv_wall.unwrap_or(0).to_le_bytes());
530        }
531        if layout.recv_mono {
532            packet.extend_from_slice(&timestamps.recv_mono.unwrap_or(0).to_le_bytes());
533        }
534        if layout.midpoint_wall {
535            packet.extend_from_slice(&timestamps.midpoint_wall.unwrap_or(0).to_le_bytes());
536        }
537        if layout.midpoint_mono {
538            packet.extend_from_slice(&timestamps.midpoint_mono.unwrap_or(0).to_le_bytes());
539        }
540        if layout.send_wall {
541            packet.extend_from_slice(&timestamps.send_wall.unwrap_or(0).to_le_bytes());
542        }
543        if layout.send_mono {
544            packet.extend_from_slice(&timestamps.send_mono.unwrap_or(0).to_le_bytes());
545        }
546
547        packet.resize(packet_len, 0);
548        packet
549    }
550
551    fn recv_event_with_timeout(sub: &EventSubscription) -> ClientEvent {
552        let deadline = Instant::now() + Duration::from_secs(2);
553        loop {
554            match sub.try_recv() {
555                Ok(Some(event)) => return event,
556                Ok(None) if Instant::now() < deadline => {
557                    thread::sleep(Duration::from_millis(1));
558                }
559                Ok(None) => panic!("timed out waiting for managed client event"),
560                Err(err) => panic!("subscription ended while waiting for event: {err}"),
561            }
562        }
563    }
564
565    fn collect_until_closed(sub: &EventSubscription) -> Vec<ClientEvent> {
566        let mut events = Vec::new();
567        let deadline = Instant::now() + Duration::from_secs(2);
568        while Instant::now() < deadline {
569            match sub.try_recv() {
570                Ok(Some(event)) => {
571                    let closed = matches!(event, ClientEvent::SessionClosed { .. });
572                    events.push(event);
573                    if closed {
574                        return events;
575                    }
576                }
577                Ok(None) => thread::sleep(Duration::from_millis(1)),
578                Err(_) => return events,
579            }
580        }
581        panic!("timed out waiting for session close event");
582    }
583
584    #[test]
585    fn stop_is_idempotent() {
586        let server = start_echo_server(test_params(None, Duration::from_millis(10)));
587        let session = ManagedClient::start(config(server.addr, None)).unwrap();
588        session.stop();
589        session.stop();
590        let outcome = session.join().unwrap();
591        server.join();
592
593        assert_eq!(outcome.end_reason, SessionEndReason::Cancelled);
594    }
595
596    #[test]
597    fn finite_managed_run_emits_session_probe_and_close_events() {
598        let duration = Duration::from_millis(35);
599        let server = start_echo_server(test_params(Some(duration), Duration::from_millis(10)));
600        let (session, sub) = ManagedClient::start_with_subscription(
601            config(server.addr, Some(duration)),
602            SubscriberConfig {
603                capacity: 16,
604                overflow: SubscriberOverflow::DropNewest,
605            },
606        )
607        .unwrap();
608
609        let events = collect_until_closed(&sub);
610        let outcome = session.join().unwrap();
611        server.join();
612
613        assert_eq!(outcome.end_reason, SessionEndReason::TestComplete);
614        assert!(events
615            .iter()
616            .any(|event| matches!(event, ClientEvent::SessionStarted { .. })));
617        assert!(events
618            .iter()
619            .any(|event| matches!(event, ClientEvent::EchoReply { .. })));
620        assert!(events
621            .iter()
622            .any(|event| matches!(event, ClientEvent::SessionClosed { .. })));
623    }
624
625    #[test]
626    fn finite_managed_run_drains_late_reply_after_timeout_before_close() {
627        let duration = Duration::from_millis(1);
628        let params = test_params(Some(duration), Duration::from_millis(10));
629        let server = start_delayed_reply_server(params, Duration::from_millis(60));
630        let mut cfg = config(server.addr, Some(duration));
631        cfg.probe_timeout = Duration::from_millis(20);
632
633        let (session, sub) = ManagedClient::start_with_subscription(
634            cfg,
635            SubscriberConfig {
636                capacity: 16,
637                overflow: SubscriberOverflow::DropNewest,
638            },
639        )
640        .unwrap();
641
642        let events = collect_until_closed(&sub);
643        let outcome = session.join().unwrap();
644        server.join();
645
646        assert_eq!(outcome.end_reason, SessionEndReason::TestComplete);
647        assert!(events
648            .iter()
649            .any(|event| matches!(event, ClientEvent::EchoLoss { seq: 0, .. })));
650        assert!(events.iter().any(|event| matches!(
651            event,
652            ClientEvent::LateReply {
653                seq: 0,
654                sent_at: Some(_),
655                rtt: Some(_),
656                ..
657            }
658        )));
659        let late_before_close = events
660            .iter()
661            .position(|event| matches!(event, ClientEvent::LateReply { rtt: Some(_), .. }));
662        let close = events
663            .iter()
664            .position(|event| matches!(event, ClientEvent::SessionClosed { .. }));
665        let late_before_close = late_before_close.expect("missing stats-eligible LateReply");
666        let close = close.expect("missing SessionClosed");
667        assert!(late_before_close < close);
668    }
669
670    #[test]
671    fn continuous_managed_run_can_be_stopped_cleanly() {
672        let server = start_echo_server(test_params(None, Duration::from_millis(10)));
673        let session = ManagedClient::start(config(server.addr, None)).unwrap();
674        let sub = session
675            .subscribe(SubscriberConfig {
676                capacity: 16,
677                overflow: SubscriberOverflow::DropNewest,
678            })
679            .unwrap();
680
681        loop {
682            if matches!(recv_event_with_timeout(&sub), ClientEvent::EchoReply { .. }) {
683                break;
684            }
685        }
686        session.stop();
687        let events = collect_until_closed(&sub);
688        let outcome = session.join().unwrap();
689        server.join();
690
691        assert_eq!(outcome.end_reason, SessionEndReason::Cancelled);
692        assert!(events
693            .iter()
694            .any(|event| matches!(event, ClientEvent::SessionClosed { .. })));
695    }
696
697    #[test]
698    fn join_reports_worker_panic() {
699        let hub = EventHub::new();
700        let cancellation = CancellationToken::new();
701        let worker = thread::spawn(|| -> Result<SessionOutcome, ClientError> {
702            panic!("intentional managed worker panic")
703        });
704        let session = ManagedClientSession {
705            hub,
706            cancellation,
707            worker: Some(worker),
708        };
709
710        assert!(matches!(session.join(), Err(ClientError::WorkerPanicked)));
711    }
712
713    #[test]
714    fn no_test_managed_run_returns_no_test_outcome() {
715        use crate::RunMode;
716
717        let params = test_params(Some(Duration::from_millis(10)), Duration::from_millis(10));
718        let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
719        let addr = socket.local_addr().unwrap();
720        let (tx, rx) = mpsc::channel();
721        let done = thread::spawn(move || {
722            let (request, peer) = recv_request(&socket);
723            tx.send(request[3] & flags::FLAG_CLOSE != 0).unwrap();
724            socket
725                .send_to(
726                    &open_reply(FLAG_OPEN | FLAG_REPLY | flags::FLAG_CLOSE, 0, &params),
727                    peer,
728                )
729                .unwrap();
730        });
731
732        let mut cfg = config(addr, Some(Duration::from_millis(10)));
733        cfg.run_mode = RunMode::NoTest;
734        let session = ManagedClient::start(cfg).unwrap();
735        assert!(rx.recv_timeout(Duration::from_secs(2)).unwrap());
736        let outcome = session.join().unwrap();
737        done.join().unwrap();
738
739        assert_eq!(outcome.end_reason, SessionEndReason::NoTestComplete);
740    }
741}