185 lines
7.9 KiB
Rust
185 lines
7.9 KiB
Rust
mod support;
|
|
|
|
use std::{collections::HashMap, time::Duration};
|
|
|
|
use irtt_client::{managed::*, ClientConfig, ClientEvent};
|
|
use irtt_server::ServerConfig;
|
|
use support::InTreeServer;
|
|
use tokio::time::timeout;
|
|
|
|
// A real service and public events protect cadence/fairness without depending
|
|
// on a scheduler, a send cursor, or a private event-publication hook.
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn both_pacing_modes_serve_every_target_on_its_negotiated_grid() {
|
|
timeout(Duration::from_secs(5), async {
|
|
let server = InTreeServer::start(ServerConfig::default());
|
|
for pacing in [ManagedPacing::Burst, ManagedPacing::Staggered] {
|
|
let config = ManagedClientConfig {
|
|
client: ClientConfig {
|
|
duration: Some(Duration::from_millis(600)),
|
|
interval: Duration::from_millis(100),
|
|
probe_timeout: Duration::from_millis(100),
|
|
open_timeouts: vec![Duration::from_millis(200)],
|
|
..ClientConfig::default()
|
|
},
|
|
pacing,
|
|
..ManagedClientConfig::default()
|
|
};
|
|
let targets =
|
|
["first", "second"].map(|id| ManagedTargetConfig::new(id, server.addr.to_string()));
|
|
let (task, handle) = ManagedClient::task(config, targets.into()).unwrap();
|
|
let mut events = handle.subscribe().unwrap();
|
|
let run = tokio::spawn(task);
|
|
let mut sends: HashMap<TargetInstance, Vec<_>> = HashMap::new();
|
|
let mut actual_sends = Vec::new();
|
|
while let Ok(event) = events.recv().await {
|
|
if let ManagedEvent::Client {
|
|
target,
|
|
event:
|
|
ClientEvent::EchoSent {
|
|
scheduled_at,
|
|
sent_at,
|
|
..
|
|
},
|
|
} = event
|
|
{
|
|
let scheduled = scheduled_at.expect("managed probes expose their cadence");
|
|
assert!(sent_at.mono >= scheduled);
|
|
actual_sends.push((target.clone(), sent_at.mono));
|
|
let previous = sends.entry(target).or_default();
|
|
if let Some(last) = previous.last() {
|
|
let elapsed: Duration = scheduled.duration_since(*last);
|
|
assert!(!elapsed.is_zero());
|
|
assert_eq!(elapsed.as_nanos() % Duration::from_millis(100).as_nanos(), 0);
|
|
}
|
|
previous.push(scheduled);
|
|
}
|
|
}
|
|
let outcome = run.await.unwrap();
|
|
assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete);
|
|
assert_eq!(outcome.successful_target_outcomes, 2);
|
|
assert_eq!(sends.len(), 2);
|
|
actual_sends.sort_by_key(|(_, at)| *at);
|
|
let cross_target_gaps = actual_sends.windows(2)
|
|
.filter(|pair| pair[0].0 != pair[1].0)
|
|
.map(|pair| pair[1].1.duration_since(pair[0].1)).collect::<Vec<_>>();
|
|
assert!(cross_target_gaps.len() >= 2);
|
|
// Allow timer wake jitter while separating the nominal 50 ms
|
|
// stagger spacing from back-to-back burst sends.
|
|
match pacing {
|
|
ManagedPacing::Burst => assert!(cross_target_gaps.iter().filter(|gap| **gap < Duration::from_millis(25)).count() >= 2,
|
|
"burst must repeatedly send different targets back-to-back: {cross_target_gaps:?}"),
|
|
ManagedPacing::Staggered => assert!(cross_target_gaps.iter().all(|gap| *gap >= Duration::from_millis(35)),
|
|
"staggered must space different targets across the interval: {cross_target_gaps:?}"),
|
|
}
|
|
for target in outcome.recent_target_outcomes.iter() {
|
|
assert!(sends[&target.target].len() >= 2);
|
|
assert!(target.replies_received >= 2);
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("managed pacing did not complete");
|
|
}
|
|
|
|
// Updates, receipts, generations and cleanup are observable through the control
|
|
// handle and event subscription; no task internals or artificial races are needed.
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn target_updates_preserve_identical_sessions_and_retire_removed_generations() {
|
|
timeout(Duration::from_secs(5), async {
|
|
let server = InTreeServer::start(ServerConfig::default());
|
|
let config = ManagedClientConfig {
|
|
client: ClientConfig {
|
|
duration: None,
|
|
interval: Duration::from_millis(20),
|
|
probe_timeout: Duration::from_millis(100),
|
|
open_timeouts: vec![Duration::from_millis(200)],
|
|
..ClientConfig::default()
|
|
},
|
|
completion: ManagedCompletionPolicy::ExplicitStop,
|
|
..ManagedClientConfig::default()
|
|
};
|
|
let configured = ManagedTargetConfig::new("one", server.addr.to_string());
|
|
let (task, handle) = ManagedClient::task(config, vec![]).unwrap();
|
|
let mut events = handle.subscribe().unwrap();
|
|
let run = tokio::spawn(task);
|
|
let added = handle
|
|
.update_targets(vec![configured.clone()])
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
let first = added.status.targets[0].target.clone();
|
|
loop {
|
|
if matches!(events.recv().await.unwrap(), ManagedEvent::Client {
|
|
target, event: ClientEvent::EchoReply { .. }
|
|
} if target == first)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
let identical = handle
|
|
.update_targets(vec![configured.clone()])
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(identical.status.targets.len(), 1);
|
|
assert_eq!(identical.status.targets[0].target, first);
|
|
loop {
|
|
match events.recv().await.unwrap() {
|
|
ManagedEvent::Client {
|
|
target,
|
|
event: ClientEvent::SessionStarted { .. },
|
|
} if target == first => panic!("identical update reopened the session"),
|
|
ManagedEvent::Client {
|
|
target,
|
|
event: ClientEvent::EchoReply { .. },
|
|
} if target == first => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
handle.update_targets(vec![]).unwrap().await.unwrap();
|
|
loop {
|
|
if let ManagedEvent::TargetFinished { outcome } = events.recv().await.unwrap() {
|
|
assert_eq!(outcome.target, first);
|
|
assert_eq!(outcome.end_reason, ManagedTargetEndReason::Removed);
|
|
assert_eq!(outcome.cleanup_failure, None);
|
|
break;
|
|
}
|
|
}
|
|
let readded = handle
|
|
.update_targets(vec![configured])
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
let second = readded
|
|
.status
|
|
.targets
|
|
.iter()
|
|
.find(|target| target.desired)
|
|
.unwrap()
|
|
.target
|
|
.clone();
|
|
assert!(second.generation > first.generation);
|
|
loop {
|
|
if matches!(events.recv().await.unwrap(), ManagedEvent::Client {
|
|
target, event: ClientEvent::EchoReply { .. }
|
|
} if target == second)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
let stop = handle.stop();
|
|
let outcome = run.await.unwrap();
|
|
stop.await;
|
|
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
|
|
assert_eq!(outcome.total_target_outcomes, 2);
|
|
assert!(outcome.recent_target_outcomes.iter().any(|target| {
|
|
target.target == second
|
|
&& target.end_reason == ManagedTargetEndReason::Stopped
|
|
&& target.cleanup_failure.is_none()
|
|
}));
|
|
})
|
|
.await
|
|
.expect("managed target updates did not complete");
|
|
}
|