use futures::future::{select, Either};
use std::{collections::HashSet, future::Future, time::Duration};
pub(crate) const MAX_GET_FALLBACK_PEERS: usize = 20;
pub(crate) const CLOSE_GROUP_RETRY_DELAY: Duration = Duration::from_secs(1);
pub(crate) fn is_authoritative_not_found(not_found: usize, queried: usize) -> bool {
queried >= ant_protocol::CLOSE_GROUP_MAJORITY && not_found == queried
}
pub(crate) async fn race_cached_read<T, E>(
cached: Option<impl Future<Output = Result<Option<T>, E>>>,
discovered: impl Future<Output = Result<Option<T>, E>>,
retryable: impl Fn(&E) -> bool,
) -> Result<Option<T>, E> {
let Some(cached) = cached else {
return discovered.await;
};
let remaining = match select(Box::pin(cached), Box::pin(discovered)).await {
Either::Left((result, remaining)) => match result {
Ok(Some(value)) => return Ok(Some(value)),
Err(error) if !retryable(&error) => return Err(error),
_ => remaining.await,
},
Either::Right((result, remaining)) => match result {
Ok(Some(value)) => return Ok(Some(value)),
Err(error) if !retryable(&error) => return Err(error),
_ => remaining.await,
},
};
match remaining {
Err(error) if retryable(&error) => Ok(None),
result => result,
}
}
pub(crate) struct ReadCandidates<P> {
pub closest: Vec<P>,
pub known: Vec<P>,
}
pub(crate) fn read_targets<P>(
candidates: ReadCandidates<P>,
target: &[u8; 32],
key: impl Fn(&P) -> [u8; 32],
) -> Vec<P> {
let mut primary = candidates.closest;
let mut known = candidates.known;
primary.sort_by_key(|p| ant_protocol::transport::xor_distance(&key(p), target));
known.sort_by_key(|p| ant_protocol::transport::xor_distance(&key(p), target));
let mut seen = HashSet::new();
primary.retain(|p| seen.insert(key(p)));
primary.extend(
known
.into_iter()
.filter(|p| seen.insert(key(p)))
.take(MAX_GET_FALLBACK_PEERS),
);
primary
}
pub(crate) async fn retrieve_progressive<P, T, E, D, DF, G, GF, S, SF>(
target: [u8; 32],
early_limit: usize,
discover: D,
key: impl Fn(&P) -> [u8; 32],
get: G,
retryable: impl Fn(&E) -> bool,
sleep: S,
) -> Result<Option<T>, E>
where
P: Clone,
D: Fn(tokio::sync::watch::Sender<Vec<P>>) -> DF,
DF: Future<Output = ReadCandidates<P>>,
G: Fn(P, bool) -> GF,
GF: Future<Output = Result<Option<T>, E>>,
S: Fn(Duration) -> SF,
SF: Future<Output = ()>,
{
for attempt in 0..2 {
if attempt > 0 {
sleep(CLOSE_GROUP_RETRY_DELAY).await;
}
let (sender, mut updates) = tokio::sync::watch::channel(Vec::new());
let mut discovery = Box::pin(discover(sender));
let mut attempted = HashSet::new();
let not_found = std::sync::Mutex::new(HashSet::new());
let mut active: Option<([u8; 32], std::pin::Pin<Box<GF>>)> = None;
let mut updates_open = true;
let candidates = loop {
if active.is_none() && attempted.len() < early_limit.min(MAX_GET_FALLBACK_PEERS) {
let next = updates
.borrow_and_update()
.iter()
.filter(|peer| !attempted.contains(&key(peer)))
.min_by_key(|peer| ant_protocol::transport::xor_distance(&key(peer), &target))
.cloned();
if let Some(peer) = next {
let id = key(&peer);
attempted.insert(id);
active = Some((id, Box::pin(get(peer, true))));
}
}
let event = {
let changed = async {
if updates_open {
updates.changed().await.is_ok()
} else {
futures::future::pending().await
}
};
let read = async {
match &mut active {
Some((id, future)) => (*id, future.await),
None => futures::future::pending().await,
}
};
match select(
discovery.as_mut(),
Box::pin(select(Box::pin(read), Box::pin(changed))),
)
.await
{
Either::Left((candidates, _)) => Either::Left(candidates),
Either::Right((event, _)) => Either::Right(match event {
Either::Left((result, _)) => Either::Left(result),
Either::Right((open, _)) => Either::Right(open),
}),
}
};
match event {
Either::Left(candidates) => break candidates,
Either::Right(Either::Right(open)) => updates_open = open,
Either::Right(Either::Left((id, result))) => {
active = None;
match result {
Ok(Some(value)) => return Ok(Some(value)),
Ok(None) => {
not_found
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id);
}
Err(error) if !retryable(&error) => return Err(error),
Err(_) => {}
}
}
}
};
let peers = read_targets(candidates, &target, &key);
let final_ids = peers.iter().map(&key).collect::<HashSet<_>>();
let pending_early = active.map(|(id, future)| {
let not_found = ¬_found;
async move {
let result = future.await;
if matches!(result, Ok(None)) {
not_found
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id);
}
result
}
});
let ordinary = async {
for peer in peers {
let id = key(&peer);
if attempted.contains(&id) {
continue;
}
match get(peer, false).await {
Ok(Some(value)) => return Ok(Some(value)),
Ok(None) => {
not_found
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id);
}
Err(error) if !retryable(&error) => return Err(error),
Err(_) => {}
}
}
Ok(None)
};
if let Some(value) = race_cached_read(pending_early, ordinary, &retryable).await? {
return Ok(Some(value));
}
let misses = not_found.lock().unwrap_or_else(|e| e.into_inner());
if is_authoritative_not_found(final_ids.intersection(&misses).count(), final_ids.len()) {
break;
}
}
Ok(None)
}
#[cfg(test)]
async fn retrieve<P: Clone, T, E, D, DF, G, GF, S, SF>(
target: [u8; 32],
discover: D,
key: impl Fn(&P) -> [u8; 32],
get: G,
retryable: impl Fn(&E) -> bool,
sleep: S,
) -> Result<Option<T>, E>
where
D: Fn() -> DF,
DF: Future<Output = ReadCandidates<P>>,
G: Fn(P) -> GF,
GF: Future<Output = Result<Option<T>, E>>,
S: Fn(Duration) -> SF,
SF: Future<Output = ()>,
{
retrieve_progressive(
target,
0,
|_| discover(),
key,
|peer, _| get(peer),
retryable,
sleep,
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::{Cell, RefCell};
fn peer(id: u8) -> [u8; 32] {
[id; 32]
}
#[derive(Debug, PartialEq)]
enum ReadError {
Transport,
Integrity,
}
#[test]
fn progressive_verified_read_finishes_before_discovery() {
let result = futures::executor::block_on(retrieve_progressive(
peer(0),
7,
|updates| async move {
updates.send_replace(vec![peer(1)]);
futures::future::pending().await
},
|p| *p,
|_, early| async move {
assert!(early);
Ok::<_, ReadError>(Some(42))
},
|_| true,
|_| async {},
));
assert_eq!(result, Ok(Some(42)));
}
#[test]
fn early_misses_cannot_end_incomplete_discovery() {
use futures::FutureExt;
let gets = Cell::new(0);
let result = retrieve_progressive(
peer(0),
7,
|updates| async move {
updates.send_replace((1..=7).map(peer).collect());
futures::future::pending().await
},
|p| *p,
|_, early| {
assert!(early);
gets.set(gets.get() + 1);
async { Ok::<Option<()>, ReadError>(None) }
},
|_| true,
|_| async {},
)
.now_or_never();
assert!(result.is_none());
assert_eq!(gets.get(), 7);
}
#[test]
fn slow_early_get_is_deduplicated_and_does_not_block_another_holder() {
let (send, receive) = futures::channel::oneshot::channel();
let send = RefCell::new(Some(send));
let receive = RefCell::new(Some(receive));
let gets = RefCell::new(Vec::new());
let result = futures::executor::block_on(retrieve_progressive(
peer(0),
7,
|updates| {
let receive = receive.borrow_mut().take().unwrap();
async move {
updates.send_replace(vec![peer(1)]);
receive.await.unwrap();
ReadCandidates {
closest: vec![peer(1), peer(2)],
known: vec![],
}
}
},
|p| *p,
|p, early| {
gets.borrow_mut().push((p, early));
let send = &send;
async move {
if p == peer(1) {
send.borrow_mut().take().unwrap().send(()).unwrap();
futures::future::pending().await
} else {
Ok::<_, ReadError>(Some(42))
}
}
},
|_| true,
|_| async {},
));
assert_eq!(result, Ok(Some(42)));
assert_eq!(*gets.borrow(), vec![(peer(1), true), (peer(2), false)]);
}
#[test]
fn progressive_early_budget_preserves_ordinary_fallback_and_absence() {
let (send, receive) = futures::channel::oneshot::channel();
let send = RefCell::new(Some(send));
let receive = RefCell::new(Some(receive));
let gets = RefCell::new(Vec::new());
let result = futures::executor::block_on(retrieve_progressive(
peer(0),
2,
|updates| {
let receive = receive.borrow_mut().take().unwrap();
async move {
updates.send_replace((1..=7).map(peer).collect());
receive.await.unwrap();
ReadCandidates {
closest: (1..=7).map(peer).collect(),
known: vec![],
}
}
},
|p| *p,
|p, early| {
gets.borrow_mut().push((p, early));
if p == peer(2) {
send.borrow_mut().take().unwrap().send(()).unwrap();
}
async { Ok::<Option<()>, ReadError>(None) }
},
|_| true,
|_| async {},
));
assert_eq!(result, Ok(None));
let gets = gets.borrow();
assert_eq!(gets.len(), 7);
assert_eq!(gets.iter().filter(|(_, early)| *early).count(), 2);
assert_eq!(gets.iter().map(|(p, _)| p).collect::<HashSet<_>>().len(), 7);
}
#[test]
fn early_integrity_errors_remain_fatal_before_discovery() {
let result = futures::executor::block_on(retrieve_progressive(
peer(0),
7,
|updates| async move {
updates.send_replace(vec![peer(1)]);
futures::future::pending().await
},
|p| *p,
|_, _| async { Err::<Option<()>, _>(ReadError::Integrity) },
|error| *error == ReadError::Transport,
|_| async {},
));
assert_eq!(result, Err(ReadError::Integrity));
}
#[test]
fn cached_verified_content_does_not_wait_for_discovery() {
let result = futures::executor::block_on(race_cached_read(
Some(async { Ok::<_, ReadError>(Some(42)) }),
futures::future::pending(),
|_| false,
));
assert_eq!(result, Ok(Some(42)));
}
#[test]
fn cached_misses_and_transport_errors_preserve_discovery() {
for cached in [Ok(None), Err(ReadError::Transport)] {
let result = futures::executor::block_on(race_cached_read(
Some(async { cached }),
async { Ok(Some(42)) },
|error| *error == ReadError::Transport,
));
assert_eq!(result, Ok(Some(42)));
}
}
#[test]
fn cached_integrity_errors_remain_fatal() {
let result = futures::executor::block_on(race_cached_read(
Some(async { Err::<Option<()>, _>(ReadError::Integrity) }),
futures::future::pending(),
|error| *error == ReadError::Transport,
));
assert_eq!(result, Err(ReadError::Integrity));
}
#[test]
fn an_unresponsive_cached_peer_does_not_delay_a_discovered_holder() {
let result = futures::executor::block_on(race_cached_read(
Some(futures::future::pending()),
async { Ok::<_, ReadError>(Some(42)) },
|_| false,
));
assert_eq!(result, Ok(Some(42)));
}
#[test]
fn discovered_absence_does_not_discard_a_late_cached_holder() {
let (send, receive) = futures::channel::oneshot::channel();
let result = futures::executor::block_on(race_cached_read(
Some(async { Ok::<_, ReadError>(Some(receive.await.unwrap())) }),
async {
send.send(42).unwrap();
Ok(None)
},
|_| false,
));
assert_eq!(result, Ok(Some(42)));
}
#[test]
fn cached_miss_is_not_an_authoritative_not_found_vote() {
let rounds = Cell::new(0);
let discovered = retrieve(
peer(0),
|| {
rounds.set(rounds.get() + 1);
async {
ReadCandidates {
closest: vec![peer(1)],
known: vec![],
}
}
},
|p| *p,
|_| async { Ok::<Option<()>, ReadError>(None) },
|_| true,
|_| async {},
);
let result = futures::executor::block_on(race_cached_read(
Some(async { Ok(None) }),
discovered,
|_| true,
));
assert_eq!(result, Ok(None));
assert_eq!(rounds.get(), 2);
}
#[test]
fn fallback_is_sorted_deduplicated_and_bounded() {
let targets = read_targets(
ReadCandidates {
closest: vec![peer(3), peer(1), peer(1)],
known: (0..30).rev().map(peer).collect(),
},
&peer(0),
|p| *p,
);
assert_eq!(&targets[..4], &[peer(1), peer(3), peer(0), peer(2)]);
assert_eq!(targets.len(), 22);
assert_eq!(targets.iter().collect::<HashSet<_>>().len(), 22);
}
#[test]
fn failed_discovery_still_reads_known_holder() {
let result = futures::executor::block_on(retrieve(
peer(0),
|| async {
ReadCandidates {
closest: vec![],
known: vec![peer(1)],
}
},
|p| *p,
|_| async { Ok::<_, ()>(Some(42)) },
|_| true,
|_| async {},
));
assert_eq!(result, Ok(Some(42)));
}
#[test]
fn native_retry_rules_include_thin_results_and_remote_failures() {
for (count, remote_error, expected_rounds) in
[(3, false, 2), (4, false, 1), (7, true, 2), (0, false, 2)]
{
let rounds = Cell::new(0);
let sleeps = RefCell::new(Vec::new());
let result = futures::executor::block_on(retrieve(
peer(0),
|| {
rounds.set(rounds.get() + 1);
async move {
ReadCandidates {
closest: (0..count).map(peer).collect(),
known: vec![],
}
}
},
|p| *p,
|_| async move {
if remote_error {
Err(())
} else {
Ok(None::<()>)
}
},
|_| true,
|delay| {
sleeps.borrow_mut().push(delay);
async {}
},
));
assert_eq!(result, Ok(None));
assert_eq!(rounds.get(), expected_rounds);
assert_eq!(sleeps.borrow().len(), expected_rounds - 1);
}
}
#[test]
fn retry_rediscovers_and_can_find_a_new_holder() {
let rounds = Cell::new(0);
let result = futures::executor::block_on(retrieve(
peer(0),
|| {
rounds.set(rounds.get() + 1);
let id = rounds.get();
async move {
ReadCandidates {
closest: vec![peer(id)],
known: vec![],
}
}
},
|p| *p,
|p| async move { Ok::<_, ()>((p == peer(2)).then_some(42)) },
|_| true,
|_| async {},
));
assert_eq!(result, Ok(Some(42)));
assert_eq!(rounds.get(), 2);
}
}