Merge branch 'feature/managed-terminal-outcome'
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 30s
Rust CI / cargo clippy (push) Successful in 33s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m13s
Rust CI / cargo doc (push) Successful in 26s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / actionlint (push) Successful in 2s
Supply chain / cargo-deny bans licenses sources (push) Successful in 6s
Netem Interop / netem interop (push) Successful in 1m4s

This commit is contained in:
Nils Andreas Svee committed 2026-10-04 22:05:25 +07:00
commit 236b80db1d
4 files changed
+82 -25

No files matched your search

+6
View File
@@ -141,6 +141,12 @@ pub struct ManagedTargetStatus {
pub target: TargetInstance,
pub desired: bool,
pub lifecycle: ManagedTargetLifecycle,
/// Authoritative completed accounting for this target generation.
///
/// Present if and only if `lifecycle` is [`ManagedTargetLifecycle::Terminal`].
/// This shares the terminal phase's outcome with [`ManagedEvent::TargetFinished`],
/// so terminal details remain durable even when presentation events are lost.
pub outcome: Option<Arc<ManagedTargetOutcome>>,
pub server_addr: Arc<str>,
pub remote: Option<SocketAddr>,
}
+35 -23
View File
@@ -369,7 +369,10 @@ enum TargetPhase {
primary_end: ManagedTargetEndReason,
cleanup_failure: Option<ManagedTargetFailure>,
},
Terminal,
/// Terminal state always owns its finalized authoritative outcome.
Terminal {
outcome: Arc<ManagedTargetOutcome>,
},
}
impl TargetPhase {
@@ -381,7 +384,7 @@ impl TargetPhase {
Self::Active { .. } => ManagedTargetLifecycle::Active,
Self::Draining { .. } => ManagedTargetLifecycle::Draining,
Self::Closing { .. } => ManagedTargetLifecycle::Closing,
Self::Terminal => ManagedTargetLifecycle::Terminal,
Self::Terminal { .. } => ManagedTargetLifecycle::Terminal,
}
}
@@ -589,9 +592,9 @@ struct UpdatePlan {
fn synchronously_retireable(phase: &TargetPhase) -> bool {
match phase {
TargetPhase::Pending { .. } | TargetPhase::Connecting { .. } | TargetPhase::Terminal => {
true
}
TargetPhase::Pending { .. }
| TargetPhase::Connecting { .. }
| TargetPhase::Terminal { .. } => true,
TargetPhase::Opening { open, .. } => !open.has_in_flight_work(),
TargetPhase::Active { .. } | TargetPhase::Draining { .. } | TargetPhase::Closing { .. } => {
false
@@ -601,7 +604,7 @@ fn synchronously_retireable(phase: &TargetPhase) -> bool {
fn runtime_satisfies_target(runtime: &TargetRuntime, target: &ManagedTargetConfig) -> bool {
runtime.membership.is_desired()
&& !matches!(runtime.phase(), TargetPhase::Terminal)
&& !matches!(runtime.phase(), TargetPhase::Terminal { .. })
&& runtime.config == *target
}
@@ -766,6 +769,10 @@ impl ManagedClientTask {
target: target.instance.clone(),
desired: target.membership.is_desired(),
lifecycle,
outcome: match target.phase() {
TargetPhase::Terminal { outcome } => Some(Arc::clone(outcome)),
_ => None,
},
server_addr: Arc::clone(&target.server_addr),
remote: target.remote,
}
@@ -841,13 +848,17 @@ impl ManagedClientTask {
end_reason: ManagedTargetEndReason,
cleanup_failure: Option<ManagedTargetFailure>,
) {
self.install_target_phase(index, TargetPhase::Terminal);
let outcome = self.targets[index].outcome(end_reason, cleanup_failure);
self.history.record(outcome.clone());
self.replace_status();
self.publish_event(ManagedEvent::TargetFinished {
outcome: Arc::new(outcome),
});
let outcome = Arc::new(self.targets[index].outcome(end_reason, cleanup_failure));
// Finalize history before the phase publisher installs Terminal, updates
// pacing and publishes one complete status followed by the lifecycle event.
self.history.record(outcome.as_ref().clone());
self.install_target_phase(
index,
TargetPhase::Terminal {
outcome: Arc::clone(&outcome),
},
);
self.publish_event(ManagedEvent::TargetFinished { outcome });
}
fn fail_target(&mut self, index: usize, phase: ManagedTargetFailurePhase, error: ClientError) {
@@ -955,12 +966,10 @@ impl ManagedClientTask {
target.membership = TargetMembership::Withdrawn(retirement.reason);
if retirement.synchronous {
synchronous.insert(retirement.index);
if !matches!(target.phase(), TargetPhase::Terminal) {
let outcome = target.outcome(retirement.reason.into(), None);
self.history.record(outcome.clone());
finished.push(ManagedEvent::TargetFinished {
outcome: Arc::new(outcome),
});
if !matches!(target.phase(), TargetPhase::Terminal { .. }) {
let outcome = Arc::new(target.outcome(retirement.reason.into(), None));
self.history.record(outcome.as_ref().clone());
finished.push(ManagedEvent::TargetFinished { outcome });
}
} else if was_paced_active {
// Removal is directional: it may discard an elapsed gate but never
@@ -1130,12 +1139,14 @@ impl ManagedClientTask {
fn prune_undesired_terminal(&mut self) {
if !self.targets.iter().any(|target| {
!target.membership.is_desired() && matches!(target.phase(), TargetPhase::Terminal)
!target.membership.is_desired()
&& matches!(target.phase(), TargetPhase::Terminal { .. })
}) {
return;
}
self.targets.retain(|target| {
target.membership.is_desired() || !matches!(target.phase(), TargetPhase::Terminal)
target.membership.is_desired()
|| !matches!(target.phase(), TargetPhase::Terminal { .. })
});
self.rebase_target_cursors();
self.replace_status();
@@ -1505,7 +1516,7 @@ impl ManagedClientTask {
}
}
}
TargetPhase::Terminal => false,
TargetPhase::Terminal { .. } => false,
}
}
@@ -2039,7 +2050,7 @@ impl ManagedClientTask {
fn all_targets_terminal(&self) -> bool {
self.targets
.iter()
.all(|target| matches!(target.phase(), TargetPhase::Terminal))
.all(|target| matches!(target.phase(), TargetPhase::Terminal { .. }))
}
fn next_deadline(&self) -> Option<Instant> {
@@ -2398,6 +2409,7 @@ fn build_task(
target: target.instance.clone(),
desired: target.membership.is_desired(),
lifecycle: ManagedTargetLifecycle::Pending,
outcome: None,
server_addr: Arc::clone(&target.server_addr),
remote: None,
})
@@ -1,6 +1,6 @@
mod support;
use std::{collections::HashMap, time::Duration};
use std::{collections::HashMap, sync::Arc, time::Duration};
use irtt_client::{managed::*, ClientConfig, ClientEvent, OpenPolicy, SessionRequest};
use irtt_server::ServerConfig;
@@ -186,6 +186,9 @@ async fn target_updates_preserve_identical_sessions_and_retire_removed_generatio
);
assert!(status.targets.iter().all(|target| target.target != first
|| target.lifecycle == ManagedTargetLifecycle::Terminal));
if let Some(target) = status.targets.iter().find(|target| target.target == first) {
assert!(Arc::ptr_eq(target.outcome.as_ref().unwrap(), &outcome));
}
break;
}
}
@@ -224,6 +227,12 @@ async fn target_updates_preserve_identical_sessions_and_retire_removed_generatio
);
assert!(status.targets.iter().all(|target| target.target != second
|| target.lifecycle == ManagedTargetLifecycle::Terminal));
let target = status
.targets
.iter()
.find(|target| target.target == second)
.unwrap();
assert!(Arc::ptr_eq(target.outcome.as_ref().unwrap(), &outcome));
}
ManagedEvent::Completed { outcome } => {
let status = handle.status();
@@ -454,6 +454,12 @@ fn event_loss_independence() {
let mut config = config(ManagedPacing::Staggered);
config.event_capacity = 1;
let (task, handle) = ManagedClient::task(config, vec![target("one", server.addr)]).unwrap();
let initial = handle.status();
assert_eq!(
initial.targets[0].lifecycle,
ManagedTargetLifecycle::Pending
);
assert!(initial.targets[0].outcome.is_none());
let mut events = handle.subscribe().unwrap();
let outcome = runtime().block_on(task);
assert!(matches!(
@@ -461,7 +467,31 @@ fn event_loss_independence() {
Err(broadcast::error::TryRecvError::Lagged(_))
));
assert_eq!(outcome.successful_target_outcomes, 1);
assert_eq!(handle.status().lifecycle, ManagedLifecycle::Completed);
let status = handle.status();
assert_eq!(status.lifecycle, ManagedLifecycle::Completed);
assert_eq!(status.targets.len(), 1);
let target = &status.targets[0];
assert!(target.desired);
assert_eq!(target.lifecycle, ManagedTargetLifecycle::Terminal);
let terminal = target
.outcome
.as_ref()
.expect("terminal details are durable");
assert_eq!(terminal.target, target.target);
assert_eq!(status.terminal_target_count, 1);
assert_eq!(status.total_target_outcomes, 1);
assert_eq!(status.successful_target_outcomes, 1);
assert_eq!(status.failed_target_outcomes, 0);
assert_eq!(status.peer_closed_target_outcomes, 0);
assert_eq!(status.discarded_target_outcomes, 0);
assert_eq!(
status.recent_target_outcomes.as_ref(),
&[terminal.as_ref().clone()]
);
assert_eq!(
status.recent_target_outcomes,
outcome.recent_target_outcomes
);
server.finish();
}