use std::time::Instant;
use futures::stream::{FuturesUnordered, StreamExt};
use rpc::Rpc;
use tokio::time::{Instant as TokioInstant, sleep};
use tape_core::track::types::{CompressedTrack, CompressedTrackProof};
use tape_crypto::address::Address;
use tape_crypto::Hash;
use tape_core::types::TrackNumber;
use tape_protocol::api::{
ApiError, FindTrackReq, FindTrackRes, FindTrackVersion, GetTrackByNumberReq,
GetTrackByNumberRes, GetTrackProofReq, GetTrackProofRes, GetTrackReq, GetTrackRes,
ListTracksByTapeReq, ListTracksByTapeRes,
};
use tape_protocol::Api;
use crate::bootstrap::{HEDGE_DELAY, counts_against_peer};
use crate::error::TapedriveError;
use crate::tapedrive::Tapedrive;
use crate::track::bootstrap_network_state;
impl<Blockchain: Rpc, Cluster: Api> Tapedrive<Blockchain, Cluster> {
pub async fn get_track(&self, track: &Address) -> Result<CompressedTrack, TapedriveError> {
query_track(self, track).await
}
pub async fn get_track_by_number(
&self,
tape: &Address,
track_number: TrackNumber,
) -> Result<CompressedTrack, TapedriveError> {
query_track_by_number(self, tape, track_number).await
}
pub async fn find_track(
&self,
tape: &Address,
key: Hash,
version: FindTrackVersion,
) -> Result<CompressedTrack, TapedriveError> {
query_find_track(self, tape, key, version).await
}
pub async fn list_tracks_by_tape(
&self,
tape: &Address,
cursor: Option<TrackNumber>,
limit: u32,
) -> Result<(Vec<CompressedTrack>, Option<TrackNumber>), TapedriveError> {
query_tracks_by_tape(self, tape, cursor, limit).await
}
pub async fn get_track_proof(
&self,
track: &Address,
) -> Result<CompressedTrackProof, TapedriveError> {
query_track_proof(self, track).await
}
}
pub(crate) async fn queryable_peers<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
) -> Result<Vec<Address>, TapedriveError> {
let state = bootstrap_network_state(client, None).await?;
let mut peers = Vec::with_capacity(state.current.committee.len());
for member in &state.current.committee {
if !peers.contains(&member.node) {
peers.push(member.node);
}
}
if peers.is_empty() {
return Err(TapedriveError::Peer(ApiError::Other(
"no committee peers available".into(),
)));
}
Ok(peers)
}
pub(crate) fn race_peers<PeerFuture>(
peers: Vec<Address>,
call: impl Fn(Address) -> PeerFuture,
) -> FuturesUnordered<PeerFuture>
where
PeerFuture: std::future::Future,
{
peers.into_iter().map(call).collect()
}
pub(crate) async fn query_ladder<Blockchain, Cluster, PeerFuture, AcceptFuture, T>(
client: &Tapedrive<Blockchain, Cluster>,
call: impl Fn(Address) -> PeerFuture,
accept: impl Fn(T) -> AcceptFuture,
) -> Result<T, TapedriveError>
where
Blockchain: Rpc,
Cluster: Api,
PeerFuture: std::future::Future<Output = Result<T, ApiError>>,
AcceptFuture: std::future::Future<Output = Result<Option<T>, TapedriveError>>,
{
let peers = queryable_peers(client).await?;
let ordered = client.reputation.order(&peers);
let width = client.read_options.query_fan_out.max(1);
let mut saw_not_found = false;
let mut last_error = None;
let mut launched = 0usize;
let mut pending = FuturesUnordered::new();
let launch = |index: usize, pending: &mut FuturesUnordered<_>| {
let node = ordered[index];
let started = Instant::now();
let future = call(node);
pending.push(async move { (node, started, future.await) });
};
while launched < width.min(ordered.len()) {
launch(launched, &mut pending);
launched += 1;
}
let hedge = sleep(HEDGE_DELAY);
tokio::pin!(hedge);
loop {
tokio::select! {
biased;
Some((node, started, result)) = pending.next() => {
match result {
Ok(value) => {
client.reputation.record_success(node, started.elapsed());
match accept(value).await? {
Some(accepted) => return Ok(accepted),
None => last_error = Some(ApiError::StaleTrackProof),
}
}
Err(error) => {
match counts_against_peer(&error) {
true => client.reputation.record_failure(node),
false => client.reputation.record_success(node, started.elapsed()),
}
match error {
ApiError::NotFound => saw_not_found = true,
error => last_error = Some(error),
}
if launched < ordered.len() {
launch(launched, &mut pending);
launched += 1;
hedge.as_mut().reset(TokioInstant::now() + HEDGE_DELAY);
}
}
}
}
_ = &mut hedge, if launched < ordered.len() => {
launch(launched, &mut pending);
launched += 1;
hedge.as_mut().reset(TokioInstant::now() + HEDGE_DELAY);
}
else => break,
}
}
Err(finish_peer_query(last_error, saw_not_found))
}
fn finish_peer_query(last_error: Option<ApiError>, saw_not_found: bool) -> TapedriveError {
if let Some(error) = last_error {
TapedriveError::from(error)
} else if saw_not_found {
TapedriveError::NotFound
} else {
TapedriveError::Peer(ApiError::Other(
"no responsive peers available".into(),
))
}
}
pub async fn query_track<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
track: &Address,
) -> Result<CompressedTrack, TapedriveError> {
query_ladder(
client,
|node| {
let req = GetTrackReq { track: *track };
async move { client.api.get_track(node, &req).await }
},
|res: GetTrackRes| async move { Ok(Some(res)) },
)
.await
.map(|res| res.track)
}
pub async fn query_track_by_number<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape: &Address,
track_number: TrackNumber,
) -> Result<CompressedTrack, TapedriveError> {
query_ladder(
client,
|node| {
let req = GetTrackByNumberReq { tape: *tape, track_number };
async move { client.api.get_track_by_number(node, &req).await }
},
|res: GetTrackByNumberRes| async move { Ok(Some(res)) },
)
.await
.map(|res| res.track)
}
pub async fn query_find_track<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape: &Address,
key: Hash,
version: FindTrackVersion,
) -> Result<CompressedTrack, TapedriveError> {
query_ladder(
client,
|node| {
let req = FindTrackReq {
tape: *tape,
key,
version: version.clone(),
};
async move { client.api.find_track(node, &req).await }
},
|res: FindTrackRes| async move { Ok(Some(res)) },
)
.await
.map(|res| res.track)
}
pub async fn query_tracks_by_tape<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
tape: &Address,
cursor: Option<TrackNumber>,
limit: u32,
) -> Result<(Vec<CompressedTrack>, Option<TrackNumber>), TapedriveError> {
query_ladder(
client,
|node| {
let req = ListTracksByTapeReq {
tape: *tape,
cursor,
limit,
};
async move { client.api.list_tracks_by_tape(node, &req).await }
},
|res: ListTracksByTapeRes| async move { Ok(Some(res)) },
)
.await
.map(|res| (res.tracks, res.next_cursor))
}
pub async fn query_track_proof<Blockchain: Rpc, Cluster: Api>(
client: &Tapedrive<Blockchain, Cluster>,
track: &Address,
) -> Result<CompressedTrackProof, TapedriveError> {
query_ladder(
client,
|node| {
let req = GetTrackProofReq { track: *track };
async move { client.api.get_track_proof(node, &req).await }
},
|res: GetTrackProofRes| async move {
let tape = client
.rpc()
.get_tape_by_address(&res.proof.state.tape)
.await
.map_err(TapedriveError::Rpc)?;
match tape.tracks.verify(&res.proof).is_ok() {
true => Ok(Some(res)),
false => Ok(None),
}
},
)
.await
.map(|res| res.proof)
}