use std::collections::HashMap;
use std::future::{poll_fn, Future};
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use nym_credentials_interface::TicketType;
use nym_task::ShutdownToken;
use tokio::sync::oneshot;
use crate::in_flight::global_data::{GlobalData, GlobalDataRequest};
use crate::traits::{CredentialFetcher, CredentialFetcherError, CredentialPublicDataFetcher};
use crate::NymCredential;
pub(crate) mod global_data;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum FetchKey {
Ticketbook(TicketType),
GlobalData(GlobalDataRequest),
}
impl From<TicketType> for FetchKey {
fn from(ticket_type: TicketType) -> Self {
Self::Ticketbook(ticket_type)
}
}
impl From<GlobalDataRequest> for FetchKey {
fn from(request: GlobalDataRequest) -> Self {
Self::GlobalData(request)
}
}
pub(crate) enum FetchJob {
Ticketbook {
ticket_type: TicketType,
fetcher: Arc<dyn CredentialFetcher>,
},
GlobalData {
request: GlobalDataRequest,
fetcher: Arc<dyn CredentialPublicDataFetcher>,
},
}
impl FetchJob {
fn key(&self) -> FetchKey {
match self {
FetchJob::Ticketbook { ticket_type, .. } => (*ticket_type).into(),
FetchJob::GlobalData { request, .. } => (*request).into(),
}
}
async fn run(self) -> Result<FetchedData, CredentialFetcherError> {
match self {
FetchJob::Ticketbook {
ticket_type,
fetcher,
} => fetcher
.fetch_ticketbooks(ticket_type)
.await
.map(FetchedData::Ticketbooks),
FetchJob::GlobalData { request, fetcher } => {
request.fetch(&*fetcher).await.map(FetchedData::GlobalData)
}
}
}
}
pub(crate) enum FetchedData {
Ticketbooks(Vec<NymCredential>),
GlobalData(GlobalData),
}
type FinishedFetch = Option<Result<FetchedData, CredentialFetcherError>>;
pub(crate) type FetchResult = Result<FinishedFetch, oneshot::error::RecvError>;
struct TrackedFetch {
cancel: ShutdownToken,
result: oneshot::Receiver<FinishedFetch>,
}
#[derive(Default)]
pub(crate) struct InFlightFetches {
fetches: HashMap<FetchKey, TrackedFetch>,
}
impl InFlightFetches {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn is_empty(&self) -> bool {
self.fetches.is_empty()
}
pub(crate) fn is_in_flight(&self, key: impl Into<FetchKey>) -> bool {
self.fetches.contains_key(&key.into())
}
pub(crate) fn in_flight_ticketbooks(&self) -> Vec<TicketType> {
self.fetches
.keys()
.filter_map(|key| match key {
FetchKey::Ticketbook(ticket_type) => Some(*ticket_type),
FetchKey::GlobalData(_) => None,
})
.collect()
}
pub(crate) fn in_flight_global_data(&self) -> Vec<GlobalDataRequest> {
self.fetches
.keys()
.filter_map(|key| match key {
FetchKey::GlobalData(request) => Some(*request),
FetchKey::Ticketbook(_) => None,
})
.collect()
}
pub(crate) fn spawn(&mut self, job: FetchJob) {
let key = job.key();
if self.is_in_flight(key) {
tracing::warn!("a {key:?} fetch is already in flight; not spawning a duplicate");
return;
}
let cancel = ShutdownToken::new();
let (tx, result) = oneshot::channel();
self.fetches.insert(
key,
TrackedFetch {
cancel: cancel.clone(),
result,
},
);
nym_task::spawn_future(async move {
let finished = cancel.run_until_cancelled(job.run()).await;
let _ = tx.send(finished);
});
}
pub(crate) async fn next_result(&mut self) -> (FetchKey, FetchResult) {
let (key, result) = poll_fn(|cx| {
for (key, fetch) in self.fetches.iter_mut() {
if let Poll::Ready(res) = Pin::new(&mut fetch.result).poll(cx) {
return Poll::Ready((*key, res));
}
}
Poll::Pending
})
.await;
self.fetches.remove(&key);
(key, result)
}
pub(crate) fn cancel_all(&self) {
for fetch in self.fetches.values() {
fetch.cancel.cancel();
}
}
pub(crate) async fn cancel_and_join(&mut self) {
self.cancel_all();
while !self.is_empty() {
let _ = self.next_result().await;
}
}
}
impl Drop for InFlightFetches {
fn drop(&mut self) {
self.cancel_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::FetcherErrorKind;
use crate::traits::{CredentialPublicDataFetcher, FetcherError};
use async_trait::async_trait;
use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey;
use nym_credentials::ecash::bandwidth::serialiser::signatures::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures,
};
use nym_ecash_time::Date;
use nym_validator_client::nym_api::EpochId;
const TYPE: TicketType = TicketType::V1MixnetEntry;
fn key() -> FetchKey {
FetchKey::Ticketbook(TYPE)
}
#[derive(Debug, thiserror::Error)]
#[error("mock fetch failure")]
struct MockFetchError;
impl FetcherError for MockFetchError {
fn kind(&self) -> FetcherErrorKind {
FetcherErrorKind::Other
}
}
enum Behaviour {
Succeed,
Fail,
Hang,
Panic,
}
struct MockFetcher {
behaviour: Behaviour,
}
fn fetcher(behaviour: Behaviour) -> Arc<dyn CredentialFetcher> {
Arc::new(MockFetcher { behaviour })
}
fn job(ticket_type: TicketType, behaviour: Behaviour) -> FetchJob {
FetchJob::Ticketbook {
ticket_type,
fetcher: fetcher(behaviour),
}
}
#[async_trait]
impl CredentialFetcher for MockFetcher {
async fn fetch_ticketbooks(
&self,
_ticketbook_type: TicketType,
) -> Result<Vec<NymCredential>, CredentialFetcherError> {
match self.behaviour {
Behaviour::Succeed => Ok(Vec::new()),
Behaviour::Fail => Err(MockFetchError.into()),
Behaviour::Hang => std::future::pending().await,
Behaviour::Panic => panic!("mock fetch panic"),
}
}
async fn cleanup(&self) {}
async fn reset(self) -> Result<(), CredentialFetcherError> {
Ok(())
}
}
#[async_trait]
impl CredentialPublicDataFetcher for MockFetcher {
async fn fetch_master_verification_key(
&self,
_epoch_id: EpochId,
) -> Result<EpochVerificationKey, CredentialFetcherError> {
Err(MockFetchError.into())
}
async fn fetch_coin_index_signatures(
&self,
_epoch_id: EpochId,
) -> Result<AggregatedCoinIndicesSignatures, CredentialFetcherError> {
Err(MockFetchError.into())
}
async fn fetch_expiration_date_signatures(
&self,
_expiration_date: Date,
_epoch_id: EpochId,
) -> Result<AggregatedExpirationDateSignatures, CredentialFetcherError> {
Err(MockFetchError.into())
}
}
#[tokio::test]
async fn tracks_a_spawned_fetch() {
let mut fetches = InFlightFetches::new();
assert!(fetches.is_empty());
fetches.spawn(job(TYPE, Behaviour::Hang));
assert!(fetches.is_in_flight(TYPE));
assert!(!fetches.is_empty());
}
#[tokio::test]
async fn completed_fetch_bubbles_up_and_frees_slot() {
let mut fetches = InFlightFetches::new();
fetches.spawn(job(TYPE, Behaviour::Succeed));
let (fetched_key, result) = fetches.next_result().await;
assert_eq!(fetched_key, key());
assert!(matches!(result, Ok(Some(Ok(FetchedData::Ticketbooks(_))))));
assert!(fetches.is_empty());
}
#[tokio::test]
async fn failed_fetch_bubbles_up_as_ok_some_err() {
let mut fetches = InFlightFetches::new();
fetches.spawn(job(TYPE, Behaviour::Fail));
let (fetched_key, result) = fetches.next_result().await;
assert_eq!(fetched_key, key());
assert!(matches!(result, Ok(Some(Err(_)))));
assert!(fetches.is_empty());
}
#[tokio::test]
async fn cancelled_fetch_bubbles_up_as_ok_none() {
let mut fetches = InFlightFetches::new();
fetches.spawn(job(TYPE, Behaviour::Hang));
fetches.cancel_all();
let (fetched_key, result) = fetches.next_result().await;
assert_eq!(fetched_key, key());
assert!(matches!(result, Ok(None)));
assert!(fetches.is_empty());
}
#[tokio::test]
async fn spawning_a_duplicate_type_is_refused_and_leaves_the_first_fetch_intact() {
let mut fetches = InFlightFetches::new();
fetches.spawn(job(TYPE, Behaviour::Hang));
fetches.spawn(job(TYPE, Behaviour::Succeed));
assert_eq!(fetches.fetches.len(), 1);
fetches.cancel_all();
let (fetched_key, result) = fetches.next_result().await;
assert_eq!(fetched_key, key());
assert!(matches!(result, Ok(None)));
}
#[tokio::test]
async fn cancel_and_join_drains_everything_and_empties_the_map() {
let mut fetches = InFlightFetches::new();
fetches.spawn(job(TicketType::V1MixnetEntry, Behaviour::Hang));
fetches.spawn(job(TicketType::V1MixnetExit, Behaviour::Hang));
fetches.cancel_and_join().await;
assert!(fetches.is_empty());
}
#[tokio::test]
async fn panicked_fetch_bubbles_up_as_recv_error_and_frees_slot() {
let mut fetches = InFlightFetches::new();
fetches.spawn(job(TYPE, Behaviour::Panic));
let (fetched_key, result) = fetches.next_result().await;
assert_eq!(fetched_key, key());
assert!(result.is_err());
assert!(fetches.is_empty());
}
}