fix(cli): drive single-target runs with ManagedClient

Replace the manual single-target ClientSession loops in the stream and TUI adapters with ManagedClient::start_with_subscription so the existing worker owns opening, deadline-aware pacing, receive handling, timeout polling, draining, and close.

Use bounded subscription waits for interruption and input polling, drain queued events before and after join, and report per-subscription drops without changing output, statistics, labels, summaries, warning, or peer-close policy. Add process and unit regressions for 10 ms cadence, interruption, final-event draining, TUI terminal state, and overflow visibility.
This commit is contained in:
Nils Andreas Svee committed 2026-07-28 14:42:02 +07:00
1 parent 080d45ca67
commit 6af4663b6e
9 files changed
+530 -477

No files matched your search

+53
View File
@@ -4,6 +4,7 @@ use std::{
atomic::{AtomicU64, Ordering},
Arc, Condvar, Mutex, Weak,
},
time::{Duration, Instant},
};
use crate::{error::EventSubscriptionError, event::ClientEvent};
@@ -254,6 +255,39 @@ impl<T> EventSubscription<T> {
}
}
/// Wait up to `timeout` for the next queued event.
///
/// Returns `Ok(None)` when the timeout elapses while the subscription
/// remains connected. If the subscription is disconnected, this returns
/// [`EventSubscriptionError::Disconnected`] after any already queued events
/// have been drained.
pub fn recv_timeout(&self, timeout: Duration) -> Result<Option<T>, EventSubscriptionError> {
let started_at = Instant::now();
let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
loop {
if let Some(event) = state.queue.pop_front() {
return Ok(Some(event));
}
if !state.connected {
return Err(EventSubscriptionError::Disconnected);
}
let Some(wait_for) = timeout.checked_sub(started_at.elapsed()) else {
return Ok(None);
};
if wait_for.is_zero() {
return Ok(None);
}
let (next_state, _) = self
.inner
.available
.wait_timeout(state, wait_for)
.expect("subscriber mutex poisoned");
state = next_state;
}
}
/// Try to receive one queued event without blocking.
///
/// Returns `Ok(None)` when the subscription is still connected but no event
@@ -507,6 +541,25 @@ mod tests {
);
}
#[test]
fn timed_receive_drains_queued_events_and_observes_disconnect() {
let hub = EventHub::new();
let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
assert_eq!(sub.recv_timeout(Duration::ZERO).unwrap(), None);
hub.publish(event(1));
hub.disconnect_all();
assert_eq!(
event_message(sub.recv_timeout(Duration::from_secs(1)).unwrap().unwrap()),
"event-1"
);
assert_eq!(
sub.recv_timeout(Duration::from_secs(1)).unwrap_err(),
EventSubscriptionError::Disconnected
);
}
#[test]
fn sealed_hub_drains_queued_events_then_disconnects() {
let hub = EventHub::new();
+37 -1
View File
@@ -358,7 +358,7 @@ impl OutcomeCounters {
#[cfg(test)]
mod tests {
use super::*;
use crate::{config::NegotiationPolicy, SubscriberOverflow};
use crate::{config::NegotiationPolicy, EventSubscriptionError, SubscriberOverflow};
use irtt_proto::{
echo_packet_len,
flags::{self, FLAG_OPEN, FLAG_REPLY},
@@ -687,6 +687,42 @@ mod tests {
.any(|event| matches!(event, ClientEvent::SessionClosed { .. })));
}
#[test]
fn joining_before_consuming_preserves_queued_final_events() {
let duration = Duration::from_millis(25);
let server = start_echo_server(test_params(Some(duration), Duration::from_millis(10)));
let (session, sub) = ManagedClient::start_with_subscription(
config(server.addr, Some(duration)),
SubscriberConfig {
capacity: 32,
overflow: SubscriberOverflow::DropOldest,
},
)
.unwrap();
let outcome = session.join().unwrap();
let mut events = Vec::new();
loop {
match sub.try_recv() {
Ok(Some(event)) => events.push(event),
Ok(None) => panic!("joined managed subscription remained connected"),
Err(EventSubscriptionError::Disconnected) => break,
}
}
server.join();
assert_eq!(outcome.end_reason, SessionEndReason::TestComplete);
assert_eq!(sub.dropped_events(), 0);
assert!(matches!(
events.first(),
Some(ClientEvent::SessionStarted { .. })
));
assert!(matches!(
events.last(),
Some(ClientEvent::SessionClosed { .. })
));
}
#[test]
fn finite_managed_run_drains_late_reply_after_timeout_before_close() {
let duration = Duration::from_millis(1);