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:
1 parent
080d45ca67
commit
6af4663b6e
9 files changed
+530
-477
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user