#![forbid(unsafe_code)]
use std::net::SocketAddr;
use crate::lightclient::select::SURVEY_WAVE_WIDTH;
pub(crate) trait SpeedPrioritized {
type Target: Send;
type Outcome: Send;
fn targets(&self) -> Vec<Self::Target>;
fn probe(
&self,
dial: zingo_netutils::conduit::ConduitDial,
target: Self::Target,
) -> impl std::future::Future<Output = Self::Outcome> + Send;
fn settled(&self, outcomes: &[Self::Outcome]) -> bool;
fn answered(&self, outcomes: &[Self::Outcome]) -> bool;
fn acquire(
&self,
) -> impl std::future::Future<
Output = Result<(Member, SocketAddr), crate::mixnet::acquire::TransportError>,
> + Send;
fn dispose(&self, spent: Member);
fn abandon(&self, dead: Member);
fn narrate(&self, phase: SpeedProgress) {
let _ = phase;
}
}
pub(crate) type Member = crate::mixnet::pools::Member<crate::mixnet::MixnetProxy>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SpeedProgress {
Acquiring {
draw: usize,
},
ExitAbandoned {
draw: usize,
},
}
pub(crate) async fn run_speed_prioritized<T: SpeedPrioritized>(
operation: &T,
) -> Result<(Vec<T::Outcome>, Member), SpeedError> {
let deadline = tokio::time::Instant::now() + zingo_netutils::time::SPEED_ACQUISITION_DEADLINE;
let refusal = |draws: usize| SpeedError::DeadlineExhausted {
draws,
budget: zingo_netutils::time::SPEED_ACQUISITION_DEADLINE,
};
for draw in 1..=MAX_SPEED_EXIT_DRAWS {
operation.narrate(SpeedProgress::Acquiring { draw });
let Ok(acquired) = tokio::time::timeout_at(deadline, operation.acquire()).await else {
return Err(refusal(draw - 1));
};
let (member, socks5) = acquired?;
let conduit = zingo_netutils::conduit::MixnetConduit::over(socks5);
let Ok(wave) = tokio::time::timeout_at(deadline, run_wave(operation, &conduit)).await
else {
member.retire().await;
return Err(refusal(draw));
};
match wave {
WaveEnd::Exhausted(outcomes) if !operation.answered(&outcomes) => {
operation.narrate(SpeedProgress::ExitAbandoned { draw });
operation.abandon(member);
}
WaveEnd::Settled(outcomes) | WaveEnd::Exhausted(outcomes) => {
return Ok((outcomes, member));
}
}
}
Err(SpeedError::NoLiveExit {
draws: MAX_SPEED_EXIT_DRAWS,
budget: zingo_netutils::time::SENTINEL_BUDGET,
})
}
#[derive(Debug, thiserror::Error)]
pub enum SpeedError {
#[error(transparent)]
Transport(#[from] crate::mixnet::acquire::TransportError),
#[error(
"no drawn exit carried a round trip: {draws} exits each given {}ms",
budget.as_millis()
)]
NoLiveExit {
draws: usize,
budget: std::time::Duration,
},
#[error(
"the speed-prioritized operation exceeded its {}s acquisition deadline after {draws} draws",
budget.as_secs()
)]
DeadlineExhausted {
draws: usize,
budget: std::time::Duration,
},
}
pub(crate) const MAX_SPEED_EXIT_DRAWS: usize = 6;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum WaveEnd<O> {
Settled(Vec<O>),
Exhausted(Vec<O>),
}
pub(crate) async fn run_wave<T: SpeedPrioritized>(
operation: &T,
conduit: &zingo_netutils::conduit::MixnetConduit,
) -> WaveEnd<T::Outcome> {
use futures::StreamExt as _;
let mut outcomes: Vec<T::Outcome> = Vec::new();
let mut legs = futures::stream::iter(operation.targets())
.map(|target| operation.probe(conduit.dial(), target))
.buffer_unordered(SURVEY_WAVE_WIDTH);
while let Some(outcome) = legs.next().await {
outcomes.push(outcome);
if operation.settled(&outcomes) {
return WaveEnd::Settled(outcomes);
}
}
WaveEnd::Exhausted(outcomes)
}
#[cfg(test)]
mod tests {
use super::*;
struct HangingAcquire;
impl SpeedPrioritized for HangingAcquire {
type Target = usize;
type Outcome = bool;
fn targets(&self) -> Vec<usize> {
Vec::new()
}
fn probe(
&self,
_dial: zingo_netutils::conduit::ConduitDial,
_target: usize,
) -> impl std::future::Future<Output = bool> + Send {
std::future::ready(false)
}
fn settled(&self, _outcomes: &[bool]) -> bool {
false
}
fn answered(&self, _outcomes: &[bool]) -> bool {
false
}
fn acquire(
&self,
) -> impl std::future::Future<
Output = Result<(Member, SocketAddr), crate::mixnet::acquire::TransportError>,
> + Send {
std::future::pending()
}
fn dispose(&self, _spent: Member) {}
fn abandon(&self, _dead: Member) {}
fn narrate(&self, _phase: SpeedProgress) {}
}
#[tokio::test(start_paused = true)]
async fn the_shared_deadline_ends_a_hanging_acquisition() {
let outcome = tokio::time::timeout(
zingo_netutils::time::SPEED_ACQUISITION_DEADLINE
+ zingo_netutils::time::SENTINEL_BUDGET,
run_speed_prioritized(&HangingAcquire),
)
.await;
let Ok(Err(refusal)) = outcome else {
panic!("the run must refuse at its deadline, not hang past it");
};
assert!(
matches!(
refusal,
SpeedError::DeadlineExhausted { budget, .. }
if budget == zingo_netutils::time::SPEED_ACQUISITION_DEADLINE
),
"the refusal names the ruled deadline, got: {refusal}"
);
}
struct Scripted {
answers: Vec<bool>,
}
impl SpeedPrioritized for Scripted {
type Target = usize;
type Outcome = bool;
fn targets(&self) -> Vec<usize> {
(0..self.answers.len()).collect()
}
fn probe(
&self,
_dial: zingo_netutils::conduit::ConduitDial,
target: usize,
) -> impl std::future::Future<Output = bool> + Send {
let answered = self.answers[target];
async move { answered }
}
fn settled(&self, outcomes: &[bool]) -> bool {
outcomes.iter().any(|answered| *answered)
}
fn answered(&self, outcomes: &[bool]) -> bool {
outcomes.iter().any(|answered| *answered)
}
async fn acquire(
&self,
) -> Result<(Member, SocketAddr), crate::mixnet::acquire::TransportError> {
Err(crate::mixnet::acquire::TransportError::NoAcquirer)
}
fn dispose(&self, _spent: Member) {}
fn abandon(&self, _dead: Member) {}
}
#[test]
fn the_wave_runs_at_full_width() {
assert_eq!(SURVEY_WAVE_WIDTH, 4);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_wave_ends_when_its_operation_settles() {
let conduit = zingo_netutils::conduit::MixnetConduit::over(
"127.0.0.1:1".parse().expect("a static address"),
);
let settles = Scripted {
answers: vec![false, true, false],
};
match run_wave(&settles, &conduit).await {
WaveEnd::Settled(outcomes) => {
assert!(outcomes.iter().any(|answered| *answered));
assert!(outcomes.len() <= 3, "the wave stops at the settling leg");
}
other => panic!("a settling operation ended as {other:?}"),
}
let never = Scripted {
answers: vec![false, false],
};
match run_wave(&never, &conduit).await {
WaveEnd::Exhausted(outcomes) => assert_eq!(outcomes.len(), 2),
other => panic!("an unsettled operation ended as {other:?}"),
}
}
}