fix(managed): publish terminal outcomes for failed targets

Extend the managed group stream with authoritative per-target terminal outcomes and structured failure classifications. Final client events are published first, followed by exactly one TargetFinished outcome for success, peer close, no-test, opening/runtime failure, removal, or cancellation.

Scheduler-owned cleanup now stops the receiver and seals subscriptions on natural completion, eliminating the event-consumer/join circular wait while preserving queued drain. TargetEventSubscription now yields ManagedGroupEvent, so consumers must handle Client and TargetFinished variants.

Strict-timeout regressions cover mixed success/failure, all-open failure, runtime failure, no-test ordering, removal, cancellation, and disconnection before join.
This commit is contained in:
Nils Andreas Svee committed 2026-07-28 00:07:18 +07:00
1 parent 3e0047e505
commit 97aac829ec
5 files changed
+548 -132

No files matched your search

+38 -35
View File
@@ -10,7 +10,7 @@ use std::{
use irtt_client::{
ClientEvent, EventSubscriptionError, ManagedClientGroup, ManagedClientGroupConfig,
ManagedGroupEndReason, SubscriberConfig, SubscriberOverflow, TargetEvent,
ManagedGroupEndReason, ManagedGroupEvent, SubscriberConfig, SubscriberOverflow, TargetEvent,
};
#[cfg(all(test, feature = "stats"))]
use irtt_client::{ClientTimestamp, PacketMeta, RttSample, SignedDuration};
@@ -324,23 +324,31 @@ fn run_group_stream(
}
match events.try_recv() {
Ok(Some(target_event)) => {
Ok(Some(group_event)) => {
last_event_at = Instant::now();
saw_target_event = true;
let label = target_event.target.as_str();
if is_terminal_target_event(&target_event.event) {
terminal_targets.insert(label.to_owned());
}
#[cfg(feature = "stats")]
{
let stats = stats
.entry(label.to_owned())
.or_insert_with(|| StatsCollector::new(stats_config(continuous)));
print_target_event_with_stats(&mut stream_output, &target_event, stats)?;
}
#[cfg(not(feature = "stats"))]
{
print_target_event_with_stats(&mut stream_output, &target_event)?;
match group_event {
ManagedGroupEvent::Client(target_event) => {
#[cfg(feature = "stats")]
{
let label = target_event.target.as_str();
let stats = stats
.entry(label.to_owned())
.or_insert_with(|| StatsCollector::new(stats_config(continuous)));
print_target_event_with_stats(
&mut stream_output,
&target_event,
stats,
)?;
}
#[cfg(not(feature = "stats"))]
{
print_target_event_with_stats(&mut stream_output, &target_event)?;
}
}
ManagedGroupEvent::TargetFinished(target) => {
terminal_targets.insert(target.id.as_str().to_owned());
}
}
}
Ok(None) => {
@@ -365,18 +373,20 @@ fn run_group_stream(
}
let outcome = session.join()?;
while let Ok(Some(target_event)) = events.try_recv() {
let label = target_event.target.as_str();
#[cfg(feature = "stats")]
{
let stats = stats
.entry(label.to_owned())
.or_insert_with(|| StatsCollector::new(stats_config(continuous)));
print_target_event_with_stats(&mut stream_output, &target_event, stats)?;
}
#[cfg(not(feature = "stats"))]
{
print_target_event_with_stats(&mut stream_output, &target_event)?;
while let Ok(Some(group_event)) = events.try_recv() {
if let ManagedGroupEvent::Client(target_event) = group_event {
#[cfg(feature = "stats")]
{
let label = target_event.target.as_str();
let stats = stats
.entry(label.to_owned())
.or_insert_with(|| StatsCollector::new(stats_config(continuous)));
print_target_event_with_stats(&mut stream_output, &target_event, stats)?;
}
#[cfg(not(feature = "stats"))]
{
print_target_event_with_stats(&mut stream_output, &target_event)?;
}
}
}
@@ -405,13 +415,6 @@ fn run_group_stream(
Ok(())
}
fn is_terminal_target_event(event: &ClientEvent) -> bool {
matches!(
event,
ClientEvent::SessionClosed { .. } | ClientEvent::NoTestCompleted { .. }
)
}
fn estimated_group_completion_grace(args: &ClientArgs) -> Duration {
let open_timeout: Duration = args.to_client_config().open_timeouts.iter().sum();
open_timeout