refactor(client): commit probe sends transactionally
This commit is contained in:
6 files changed
+901
-169
No files matched your search
@@ -12,13 +12,17 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::schedule::{advance_cadence, ProbeSchedule},
|
||||
client::{
|
||||
echo_sent_event,
|
||||
schedule::{advance_cadence, ProbeSchedule},
|
||||
validate_datagram_length,
|
||||
},
|
||||
config::{ClientAuthConfig, ClientConfig},
|
||||
error::ClientError,
|
||||
event::{ClientEvent, OpenOutcome},
|
||||
metadata::ReceiveMeta,
|
||||
receive::recv_datagram_from,
|
||||
session::machine::{params_from_config, SendProbeResult, SessionMachine},
|
||||
session::machine::{params_from_config, SessionMachine},
|
||||
socket::{bind_unconnected_udp_socket, validate_open_timeouts},
|
||||
socket_options::apply_dscp_to_socket,
|
||||
timing::ClientTimestamp,
|
||||
@@ -633,6 +637,10 @@ struct TargetState {
|
||||
counters: TargetCounters,
|
||||
order: u64,
|
||||
final_reason: Option<ManagedTargetEndReason>,
|
||||
#[cfg(test)]
|
||||
probe_reported_len: Option<usize>,
|
||||
#[cfg(test)]
|
||||
probe_send_error: bool,
|
||||
}
|
||||
|
||||
type ScheduledTarget = (Arc<Mutex<TargetState>>, Instant);
|
||||
@@ -727,6 +735,10 @@ impl TargetState {
|
||||
counters: TargetCounters::default(),
|
||||
order,
|
||||
final_reason: None,
|
||||
#[cfg(test)]
|
||||
probe_reported_len: None,
|
||||
#[cfg(test)]
|
||||
probe_send_error: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1311,28 +1323,7 @@ fn send_echo_to_target(
|
||||
if !matches!(target.status, TargetStatus::Active) {
|
||||
return;
|
||||
}
|
||||
let remote = target.remote;
|
||||
target
|
||||
.runtime
|
||||
.ensure_open()
|
||||
.expect("active targets are open");
|
||||
let TargetState {
|
||||
runtime, schedule, ..
|
||||
} = &mut *target;
|
||||
let schedule = schedule
|
||||
.as_mut()
|
||||
.expect("active targets always have a probe schedule");
|
||||
let result = runtime.send_probe_for_deadline(schedule, None, scheduled_at, |packet| {
|
||||
let sent_at = ClientTimestamp::now();
|
||||
let send_call_start = Instant::now();
|
||||
let bytes = socket.send_to(packet, remote)?;
|
||||
let send_call = send_call_start.elapsed();
|
||||
Ok(SendProbeResult {
|
||||
sent_at,
|
||||
bytes,
|
||||
send_call,
|
||||
})
|
||||
});
|
||||
let result = send_echo_to_locked_target(socket, &mut target, scheduled_at);
|
||||
|
||||
match result {
|
||||
Ok(events) => {
|
||||
@@ -1348,6 +1339,77 @@ fn send_echo_to_target(
|
||||
}
|
||||
}
|
||||
|
||||
fn send_echo_to_locked_target(
|
||||
socket: &UdpSocket,
|
||||
target: &mut TargetState,
|
||||
scheduled_at: Instant,
|
||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||
send_echo_to_locked_target_at(socket, target, scheduled_at, ClientTimestamp::now())
|
||||
}
|
||||
|
||||
fn send_echo_to_locked_target_at(
|
||||
socket: &UdpSocket,
|
||||
target: &mut TargetState,
|
||||
scheduled_at: Instant,
|
||||
sent_at: ClientTimestamp,
|
||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||
target.runtime.ensure_open()?;
|
||||
let remote = target.remote;
|
||||
#[cfg(test)]
|
||||
let reported_bytes = target.probe_reported_len.take();
|
||||
#[cfg(test)]
|
||||
let fail_send = std::mem::take(&mut target.probe_send_error);
|
||||
let TargetState {
|
||||
runtime, schedule, ..
|
||||
} = target;
|
||||
let schedule = schedule
|
||||
.as_mut()
|
||||
.expect("active targets always have a probe schedule");
|
||||
if !schedule.permit_probe_at(sent_at.mono) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let Some(prepared) = runtime.prepare_probe()? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let machine_commit = runtime.preflight_probe_commit(&prepared, sent_at)?;
|
||||
let schedule_commit = schedule.preflight_managed_commit(scheduled_at, sent_at.mono)?;
|
||||
let mut events = Vec::new();
|
||||
events
|
||||
.try_reserve(1)
|
||||
.map_err(|source| ClientError::AllocationFailed {
|
||||
operation: "managed probe event result",
|
||||
source,
|
||||
})?;
|
||||
|
||||
let expected_bytes = prepared.bytes.len();
|
||||
let event_scheduled_at = schedule_commit.scheduled_at;
|
||||
let timer_error = schedule_commit.timer_error;
|
||||
#[cfg(test)]
|
||||
if fail_send {
|
||||
return Err(ClientError::Socket(std::io::Error::other(
|
||||
"injected managed probe send failure",
|
||||
)));
|
||||
}
|
||||
let send_call_start = Instant::now();
|
||||
let bytes = socket.send_to(&prepared.bytes, remote)?;
|
||||
let sent = runtime.commit_probe_sent(machine_commit, bytes);
|
||||
schedule.commit(schedule_commit);
|
||||
|
||||
let send_call = send_call_start.elapsed();
|
||||
#[cfg(test)]
|
||||
let bytes = reported_bytes.unwrap_or(bytes);
|
||||
validate_datagram_length(expected_bytes, bytes)?;
|
||||
events.push(echo_sent_event(
|
||||
remote,
|
||||
sent,
|
||||
send_call,
|
||||
event_scheduled_at,
|
||||
timer_error,
|
||||
));
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
fn collect_finished_targets(shared: &GroupShared, records: &mut TargetOutcomeHistory) {
|
||||
let finished = {
|
||||
let mut registry = shared
|
||||
@@ -1853,23 +1915,29 @@ mod tests {
|
||||
let scheduled_at = Instant::now();
|
||||
let sent_at = ClientTimestamp::now();
|
||||
let sent_events = {
|
||||
let remote = target.remote;
|
||||
let TargetState {
|
||||
runtime, schedule, ..
|
||||
} = &mut target;
|
||||
runtime.send_probe_for_deadline(
|
||||
schedule.as_mut().unwrap(),
|
||||
Some(sent_at),
|
||||
scheduled_at,
|
||||
|packet| {
|
||||
Ok(SendProbeResult {
|
||||
sent_at,
|
||||
bytes: packet.len(),
|
||||
send_call: Duration::ZERO,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
let schedule = schedule.as_mut().unwrap();
|
||||
assert!(schedule.permit_probe_at(sent_at.mono));
|
||||
let prepared = runtime.prepare_probe().unwrap().unwrap();
|
||||
let machine_commit = runtime.preflight_probe_commit(&prepared, sent_at).unwrap();
|
||||
let schedule_commit = schedule
|
||||
.preflight_managed_commit(scheduled_at, sent_at.mono)
|
||||
.unwrap();
|
||||
let event_scheduled_at = schedule_commit.scheduled_at;
|
||||
let timer_error = schedule_commit.timer_error;
|
||||
let sent = runtime.commit_probe_sent(machine_commit, prepared.bytes.len());
|
||||
schedule.commit(schedule_commit);
|
||||
vec![echo_sent_event(
|
||||
remote,
|
||||
sent,
|
||||
Duration::ZERO,
|
||||
event_scheduled_at,
|
||||
timer_error,
|
||||
)]
|
||||
};
|
||||
publish_events(&hub, &mut target, sent_events);
|
||||
|
||||
ActiveTargetFixture {
|
||||
@@ -1934,6 +2002,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn active_probe_target(
|
||||
config: &ClientConfig,
|
||||
params: &Params,
|
||||
remote: SocketAddr,
|
||||
opened_at: ClientTimestamp,
|
||||
) -> TargetState {
|
||||
let mut target =
|
||||
TargetState::new(config, target("peer", remote), 0, opened_at.mono).unwrap();
|
||||
let packet = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, params);
|
||||
let reply = target.runtime.decode_open_reply(&packet).unwrap();
|
||||
let mut schedule = None;
|
||||
target
|
||||
.runtime
|
||||
.accept_open_reply(reply, opened_at, |negotiated| {
|
||||
schedule = Some(ProbeSchedule::new(opened_at.mono, negotiated)?);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
target.schedule = schedule;
|
||||
target.status = TargetStatus::Active;
|
||||
target
|
||||
}
|
||||
|
||||
fn target_outcome(
|
||||
id: &str,
|
||||
remote: SocketAddr,
|
||||
@@ -2883,6 +2974,137 @@ mod tests {
|
||||
.all(|target| target.end_reason.failure().is_some()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_probe_uses_one_timestamp_for_permission_and_schedule_commit() {
|
||||
let duration = Duration::from_secs(1);
|
||||
let interval = Duration::from_millis(100);
|
||||
let params = test_params(Some(duration), interval);
|
||||
let config = group_config(Some(duration), interval, ManagedGroupPacing::Staggered).client;
|
||||
let peer = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let remote = peer.local_addr().unwrap();
|
||||
let opened_mono = Instant::now()
|
||||
.checked_sub(Duration::from_secs(2))
|
||||
.expect("test host monotonic clock has at least two seconds of history");
|
||||
let opened_at = ClientTimestamp {
|
||||
wall: std::time::SystemTime::now(),
|
||||
mono: opened_mono,
|
||||
};
|
||||
let mut target = active_probe_target(&config, ¶ms, remote, opened_at);
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: std::time::SystemTime::now(),
|
||||
mono: opened_mono + Duration::from_millis(500),
|
||||
};
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
|
||||
let events =
|
||||
send_echo_to_locked_target_at(&socket, &mut target, opened_mono, sent_at).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[ClientEvent::EchoSent {
|
||||
scheduled_at,
|
||||
sent_at: event_sent_at,
|
||||
timer_error,
|
||||
..
|
||||
}] if *scheduled_at == sent_at.mono
|
||||
&& *event_sent_at == sent_at
|
||||
&& *timer_error == Duration::ZERO
|
||||
));
|
||||
assert_eq!(
|
||||
target
|
||||
.schedule
|
||||
.as_ref()
|
||||
.and_then(ProbeSchedule::next_send_deadline),
|
||||
Some(opened_mono + Duration::from_millis(600))
|
||||
);
|
||||
assert_eq!(target.runtime.packets_sent(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_failed_probe_send_preserves_machine_and_schedule() {
|
||||
let duration = Duration::from_secs(1);
|
||||
let interval = Duration::from_millis(100);
|
||||
let params = test_params(Some(duration), interval);
|
||||
let config = group_config(Some(duration), interval, ManagedGroupPacing::Staggered).client;
|
||||
let peer = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let remote = peer.local_addr().unwrap();
|
||||
let opened_mono = Instant::now()
|
||||
.checked_sub(Duration::from_secs(2))
|
||||
.expect("test host monotonic clock has at least two seconds of history");
|
||||
let opened_at = ClientTimestamp {
|
||||
wall: std::time::SystemTime::now(),
|
||||
mono: opened_mono,
|
||||
};
|
||||
let mut target = active_probe_target(&config, ¶ms, remote, opened_at);
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: std::time::SystemTime::now(),
|
||||
mono: opened_mono + Duration::from_millis(500),
|
||||
};
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
target.probe_send_error = true;
|
||||
|
||||
assert!(matches!(
|
||||
send_echo_to_locked_target_at(&socket, &mut target, opened_mono, sent_at),
|
||||
Err(ClientError::Socket(_))
|
||||
));
|
||||
assert_eq!(target.runtime.packets_sent(), 0);
|
||||
assert_eq!(
|
||||
target
|
||||
.schedule
|
||||
.as_ref()
|
||||
.and_then(ProbeSchedule::next_send_deadline),
|
||||
Some(opened_mono)
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
send_echo_to_locked_target_at(&socket, &mut target, opened_mono, sent_at)
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_short_probe_commits_before_presentation_length_error() {
|
||||
let duration = Duration::from_secs(1);
|
||||
let interval = Duration::from_millis(100);
|
||||
let params = test_params(Some(duration), interval);
|
||||
let config = group_config(Some(duration), interval, ManagedGroupPacing::Staggered).client;
|
||||
let peer = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let remote = peer.local_addr().unwrap();
|
||||
let opened_mono = Instant::now()
|
||||
.checked_sub(Duration::from_secs(2))
|
||||
.expect("test host monotonic clock has at least two seconds of history");
|
||||
let opened_at = ClientTimestamp {
|
||||
wall: std::time::SystemTime::now(),
|
||||
mono: opened_mono,
|
||||
};
|
||||
let mut target = active_probe_target(&config, ¶ms, remote, opened_at);
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: std::time::SystemTime::now(),
|
||||
mono: opened_mono + Duration::from_millis(500),
|
||||
};
|
||||
let expected = echo_packet_len(false, ¶ms).unwrap();
|
||||
target.probe_reported_len = Some(expected - 1);
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
send_echo_to_locked_target_at(&socket, &mut target, opened_mono, sent_at),
|
||||
Err(ClientError::DatagramLengthMismatch {
|
||||
expected: error_expected,
|
||||
actual,
|
||||
}) if error_expected == expected && actual + 1 == expected
|
||||
));
|
||||
assert_eq!(target.runtime.packets_sent(), 1);
|
||||
assert_eq!(
|
||||
target
|
||||
.schedule
|
||||
.as_ref()
|
||||
.and_then(ProbeSchedule::next_send_deadline),
|
||||
Some(opened_mono + Duration::from_millis(600))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn silent_opening_peer_times_out_all_attempts_and_publishes_once() {
|
||||
let silent_peer = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
|
||||
Reference in new issue
Block a user