|
|
|
@@ -0,0 +1,355 @@
|
|
|
|
|
use std::{
|
|
|
|
|
net::{SocketAddr, UdpSocket},
|
|
|
|
|
sync::{mpsc::sync_channel, Arc, Condvar, Mutex},
|
|
|
|
|
thread::{self, JoinHandle},
|
|
|
|
|
time::{Duration, Instant},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use irtt_proto::{
|
|
|
|
|
decode_close_request, decode_echo_request, decode_open_request, encode_echo_reply,
|
|
|
|
|
encode_open_reply, flags, EchoReply, OpenReply, ReceivedStats, StampAt, TimestampFields,
|
|
|
|
|
};
|
|
|
|
|
use tokio::{runtime::Builder, time::timeout};
|
|
|
|
|
|
|
|
|
|
use super::{
|
|
|
|
|
blocking::{join_worker, WorkerRuntime},
|
|
|
|
|
BlockingManagedClient, BlockingManagedJoinError, ManagedClientConfig, ManagedCompletionPolicy,
|
|
|
|
|
ManagedEndReason, ManagedLifecycle, ManagedPacing, ManagedTargetConfig, ManagedTargetEndReason,
|
|
|
|
|
};
|
|
|
|
|
use crate::ClientConfig;
|
|
|
|
|
|
|
|
|
|
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
|
|
|
|
|
|
|
|
|
struct Signal {
|
|
|
|
|
value: Mutex<bool>,
|
|
|
|
|
ready: Condvar,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Signal {
|
|
|
|
|
fn set(&self) {
|
|
|
|
|
*self.value.lock().unwrap() = true;
|
|
|
|
|
self.ready.notify_all();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wait(&self) {
|
|
|
|
|
let value = self.value.lock().unwrap();
|
|
|
|
|
let (value, timeout) = self
|
|
|
|
|
.ready
|
|
|
|
|
.wait_timeout_while(value, Duration::from_secs(2), |value| !*value)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(*value && !timeout.timed_out());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct EchoServer {
|
|
|
|
|
addr: SocketAddr,
|
|
|
|
|
probe: Arc<Signal>,
|
|
|
|
|
close: Arc<Signal>,
|
|
|
|
|
worker: JoinHandle<()>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EchoServer {
|
|
|
|
|
fn start() -> Self {
|
|
|
|
|
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
|
|
|
socket
|
|
|
|
|
.set_read_timeout(Some(Duration::from_secs(3)))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let addr = socket.local_addr().unwrap();
|
|
|
|
|
let probe = Arc::new(Signal {
|
|
|
|
|
value: Mutex::new(false),
|
|
|
|
|
ready: Condvar::new(),
|
|
|
|
|
});
|
|
|
|
|
let close = Arc::new(Signal {
|
|
|
|
|
value: Mutex::new(false),
|
|
|
|
|
ready: Condvar::new(),
|
|
|
|
|
});
|
|
|
|
|
let worker_probe = Arc::clone(&probe);
|
|
|
|
|
let worker_close = Arc::clone(&close);
|
|
|
|
|
let worker = thread::spawn(move || {
|
|
|
|
|
let mut buffer = [0_u8; 8192];
|
|
|
|
|
let (open_len, peer) = socket.recv_from(&mut buffer).unwrap();
|
|
|
|
|
let request = decode_open_request(&buffer[..open_len], None).unwrap();
|
|
|
|
|
let negotiated = request.params;
|
|
|
|
|
let reply = encode_open_reply(
|
|
|
|
|
&OpenReply {
|
|
|
|
|
flags: flags::FLAG_OPEN | flags::FLAG_REPLY,
|
|
|
|
|
token: TOKEN,
|
|
|
|
|
params: negotiated.clone(),
|
|
|
|
|
},
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
socket.send_to(&reply, peer).unwrap();
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let (len, packet_peer) = socket.recv_from(&mut buffer).unwrap();
|
|
|
|
|
let packet = &buffer[..len];
|
|
|
|
|
if let Ok(probe) = decode_echo_request(packet, &negotiated, None) {
|
|
|
|
|
worker_probe.set();
|
|
|
|
|
let reply = encode_echo_reply(
|
|
|
|
|
&EchoReply {
|
|
|
|
|
flags: flags::FLAG_REPLY,
|
|
|
|
|
token: TOKEN,
|
|
|
|
|
sequence: probe.sequence,
|
|
|
|
|
recv_count: None,
|
|
|
|
|
recv_window: None,
|
|
|
|
|
timestamps: TimestampFields::default(),
|
|
|
|
|
payload: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
&negotiated,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
socket.send_to(&reply, packet_peer).unwrap();
|
|
|
|
|
} else if decode_close_request(packet, None).is_ok() {
|
|
|
|
|
worker_close.set();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
Self {
|
|
|
|
|
addr,
|
|
|
|
|
probe,
|
|
|
|
|
close,
|
|
|
|
|
worker,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wait_probe(&self) {
|
|
|
|
|
self.probe.wait();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wait_close(&self) {
|
|
|
|
|
self.close.wait();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn finish(self) {
|
|
|
|
|
self.worker.join().unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn test_runtime() -> tokio::runtime::Runtime {
|
|
|
|
|
Builder::new_current_thread()
|
|
|
|
|
.enable_io()
|
|
|
|
|
.enable_time()
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn config(completion: ManagedCompletionPolicy) -> ManagedClientConfig {
|
|
|
|
|
ManagedClientConfig {
|
|
|
|
|
client: ClientConfig {
|
|
|
|
|
duration: Some(Duration::from_millis(120)),
|
|
|
|
|
interval: Duration::from_millis(20),
|
|
|
|
|
probe_timeout: Duration::from_millis(40),
|
|
|
|
|
open_timeouts: vec![Duration::from_millis(200)],
|
|
|
|
|
received_stats: ReceivedStats::None,
|
|
|
|
|
stamp_at: StampAt::None,
|
|
|
|
|
..ClientConfig::default()
|
|
|
|
|
},
|
|
|
|
|
pacing: ManagedPacing::Burst,
|
|
|
|
|
completion,
|
|
|
|
|
final_drain: Duration::from_millis(5),
|
|
|
|
|
..ManagedClientConfig::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn target(server: &EchoServer) -> ManagedTargetConfig {
|
|
|
|
|
ManagedTargetConfig::new("target", server.addr.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_stopped(outcome: &super::ManagedOutcome) {
|
|
|
|
|
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn starts_without_caller_runtime_and_finishes_naturally() {
|
|
|
|
|
let server = EchoServer::start();
|
|
|
|
|
let owner = BlockingManagedClient::start(
|
|
|
|
|
config(ManagedCompletionPolicy::FinishWhenQuiescent),
|
|
|
|
|
vec![target(&server)],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
server.wait_probe();
|
|
|
|
|
let outcome = owner.join().unwrap();
|
|
|
|
|
assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete);
|
|
|
|
|
assert_eq!(outcome.recent_target_outcomes.len(), 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
outcome.recent_target_outcomes[0].end_reason,
|
|
|
|
|
ManagedTargetEndReason::TestComplete
|
|
|
|
|
);
|
|
|
|
|
server.wait_close();
|
|
|
|
|
server.finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn external_handle_stop_ends_with_graceful_close() {
|
|
|
|
|
let server = EchoServer::start();
|
|
|
|
|
let owner = BlockingManagedClient::start(
|
|
|
|
|
config(ManagedCompletionPolicy::ExplicitStop),
|
|
|
|
|
vec![target(&server)],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let handle = owner.handle();
|
|
|
|
|
|
|
|
|
|
server.wait_probe();
|
|
|
|
|
test_runtime().block_on(async {
|
|
|
|
|
timeout(Duration::from_secs(2), handle.stop())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
});
|
|
|
|
|
let outcome = owner.join().unwrap();
|
|
|
|
|
assert_stopped(&outcome);
|
|
|
|
|
server.wait_close();
|
|
|
|
|
server.finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dynamic_update_reaches_background_worker() {
|
|
|
|
|
let server = EchoServer::start();
|
|
|
|
|
let owner = BlockingManagedClient::start(config(ManagedCompletionPolicy::ExplicitStop), vec![])
|
|
|
|
|
.unwrap();
|
|
|
|
|
let handle = owner.handle();
|
|
|
|
|
|
|
|
|
|
let acknowledgement = test_runtime().block_on(async {
|
|
|
|
|
timeout(
|
|
|
|
|
Duration::from_secs(2),
|
|
|
|
|
handle.update_targets(vec![target(&server)]).unwrap(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap()
|
|
|
|
|
.unwrap()
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(acknowledgement.sequence, 1);
|
|
|
|
|
server.wait_probe();
|
|
|
|
|
test_runtime().block_on(async {
|
|
|
|
|
timeout(Duration::from_secs(2), owner.handle().stop())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
});
|
|
|
|
|
assert_stopped(&owner.join().unwrap());
|
|
|
|
|
server.wait_close();
|
|
|
|
|
server.finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dropping_external_handles_does_not_stop_owner() {
|
|
|
|
|
let server = EchoServer::start();
|
|
|
|
|
let owner = BlockingManagedClient::start(
|
|
|
|
|
config(ManagedCompletionPolicy::FinishWhenQuiescent),
|
|
|
|
|
vec![target(&server)],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
drop(owner.handle());
|
|
|
|
|
drop(owner.handle());
|
|
|
|
|
|
|
|
|
|
server.wait_probe();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
owner.join().unwrap().end_reason,
|
|
|
|
|
ManagedEndReason::TargetsComplete
|
|
|
|
|
);
|
|
|
|
|
server.wait_close();
|
|
|
|
|
server.finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dropping_owner_stops_and_joins_worker() {
|
|
|
|
|
let server = EchoServer::start();
|
|
|
|
|
let owner = BlockingManagedClient::start(
|
|
|
|
|
config(ManagedCompletionPolicy::ExplicitStop),
|
|
|
|
|
vec![target(&server)],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let handle = owner.handle();
|
|
|
|
|
|
|
|
|
|
server.wait_probe();
|
|
|
|
|
drop(owner);
|
|
|
|
|
let status = handle.status();
|
|
|
|
|
assert_eq!(status.lifecycle, ManagedLifecycle::Completed);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
status.final_outcome.as_ref().unwrap().end_reason,
|
|
|
|
|
ManagedEndReason::StopRequested
|
|
|
|
|
);
|
|
|
|
|
server.wait_close();
|
|
|
|
|
server.finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn worker_panic_is_a_join_error() {
|
|
|
|
|
let worker = thread::spawn(|| -> super::ManagedOutcome { panic!("test worker panic") });
|
|
|
|
|
assert_eq!(
|
|
|
|
|
join_worker(worker),
|
|
|
|
|
Err(BlockingManagedJoinError::WorkerPanicked)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dropping_worker_runtime_inside_async_context_does_not_panic() {
|
|
|
|
|
test_runtime().block_on(async {
|
|
|
|
|
let runtime = Builder::new_current_thread()
|
|
|
|
|
.enable_io()
|
|
|
|
|
.enable_time()
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap();
|
|
|
|
|
drop(WorkerRuntime::new(runtime));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn worker_runtime_shutdown_is_bounded_with_blocking_work() {
|
|
|
|
|
let runtime = Builder::new_current_thread()
|
|
|
|
|
.enable_io()
|
|
|
|
|
.enable_time()
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let (started_send, started_receive) = sync_channel(0);
|
|
|
|
|
let (release_send, release_receive) = sync_channel(0);
|
|
|
|
|
|
|
|
|
|
runtime.block_on(async move {
|
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
|
|
|
started_send.send(()).unwrap();
|
|
|
|
|
release_receive.recv().unwrap();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
started_receive
|
|
|
|
|
.recv_timeout(Duration::from_secs(2))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let started = Instant::now();
|
|
|
|
|
WorkerRuntime::new(runtime).shutdown();
|
|
|
|
|
assert!(started.elapsed() < Duration::from_secs(2));
|
|
|
|
|
release_send.send(()).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn drop_after_natural_completion_preserves_outcome() {
|
|
|
|
|
let server = EchoServer::start();
|
|
|
|
|
let owner = BlockingManagedClient::start(
|
|
|
|
|
config(ManagedCompletionPolicy::FinishWhenQuiescent),
|
|
|
|
|
vec![target(&server)],
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let handle = owner.handle();
|
|
|
|
|
server.wait_probe();
|
|
|
|
|
server.wait_close();
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
|
|
|
let status = loop {
|
|
|
|
|
let status = handle.status();
|
|
|
|
|
if status.lifecycle == ManagedLifecycle::Completed && status.final_outcome.is_some() {
|
|
|
|
|
break status;
|
|
|
|
|
}
|
|
|
|
|
assert!(Instant::now() < deadline, "managed worker did not complete");
|
|
|
|
|
thread::sleep(Duration::from_millis(5));
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
status.final_outcome.as_ref().unwrap().end_reason,
|
|
|
|
|
ManagedEndReason::TargetsComplete
|
|
|
|
|
);
|
|
|
|
|
drop(owner);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
handle.status().final_outcome.as_ref().unwrap().end_reason,
|
|
|
|
|
ManagedEndReason::TargetsComplete
|
|
|
|
|
);
|
|
|
|
|
server.finish();
|
|
|
|
|
}
|
When a hostname target is still resolving as the owner is stopped,
resolve_remote_tokiohas atokio::net::lookup_hostoperation running in Tokio's blocking pool; cancelling the managed connect future does not cancel that underlying resolver call. Afterblock_onreturns, this closure implicitly drops the runtime, and Tokio's runtime destructor waits indefinitely for blocking work, sojoin()ordrop(owner)can hang despite the documented bounded cleanup. Explicitly shut down the dedicated runtime with a timeout (or in the background) after capturing the task outcome.Useful? React with 👍 / 👎.