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::traits::{CredentialFetcher, CredentialFetcherError};
use crate::NymCredential;
type CredentialResult = Option<Result<Vec<NymCredential>, CredentialFetcherError>>;
pub(crate) type FetchResult = Result<CredentialResult, oneshot::error::RecvError>;
struct InFlightFetch {
cancel: ShutdownToken,
result: oneshot::Receiver<CredentialResult>,
}
#[derive(Default)]
pub(crate) struct InFlightFetches {
fetches: HashMap<TicketType, InFlightFetch>,
}
impl InFlightFetches {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn is_empty(&self) -> bool {
self.fetches.is_empty()
}
pub(crate) fn contains(&self, ticket_type: TicketType) -> bool {
self.fetches.contains_key(&ticket_type)
}
pub(crate) fn spawn(&mut self, ticket_type: TicketType, fetcher: Arc<dyn CredentialFetcher>) {
if self.fetches.contains_key(&ticket_type) {
tracing::warn!("a {ticket_type} fetch is already in flight; not spawning a duplicate");
return;
}
let cancel = ShutdownToken::new();
let (tx, result) = oneshot::channel();
let task_cancel = cancel.clone();
nym_task::spawn_future(async move {
let res = task_cancel
.run_until_cancelled(fetcher.fetch_ticketbooks(ticket_type))
.await;
let _ = tx.send(res);
});
self.fetches
.insert(ticket_type, InFlightFetch { cancel, result });
}
pub(crate) async fn next_result(&mut self) -> (TicketType, FetchResult) {
let (ticket_type, result) = poll_fn(|cx| {
for (typ, fetch) in self.fetches.iter_mut() {
if let Poll::Ready(res) = Pin::new(&mut fetch.result).poll(cx) {
return Poll::Ready((*typ, res));
}
}
Poll::Pending
})
.await;
self.fetches.remove(&ticket_type);
(ticket_type, 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;
#[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 })
}
#[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(TYPE, fetcher(Behaviour::Hang));
assert!(fetches.contains(TYPE));
assert!(!fetches.is_empty());
}
#[tokio::test]
async fn completed_fetch_bubbles_up_and_frees_slot() {
let mut fetches = InFlightFetches::new();
fetches.spawn(TYPE, fetcher(Behaviour::Succeed));
let (typ, result) = fetches.next_result().await;
assert_eq!(typ, TYPE);
assert!(matches!(result, Ok(Some(Ok(_)))));
assert!(fetches.is_empty());
}
#[tokio::test]
async fn failed_fetch_bubbles_up_as_ok_some_err() {
let mut fetches = InFlightFetches::new();
fetches.spawn(TYPE, fetcher(Behaviour::Fail));
let (typ, result) = fetches.next_result().await;
assert_eq!(typ, TYPE);
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(TYPE, fetcher(Behaviour::Hang));
fetches.cancel_all();
let (typ, result) = fetches.next_result().await;
assert_eq!(typ, TYPE);
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(TYPE, fetcher(Behaviour::Hang));
fetches.spawn(TYPE, fetcher(Behaviour::Succeed));
assert_eq!(fetches.fetches.len(), 1);
fetches.cancel_all();
let (typ, result) = fetches.next_result().await;
assert_eq!(typ, TYPE);
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(TicketType::V1MixnetEntry, fetcher(Behaviour::Hang));
fetches.spawn(TicketType::V1MixnetExit, fetcher(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(TYPE, fetcher(Behaviour::Panic));
let (typ, result) = fetches.next_result().await;
assert_eq!(typ, TYPE);
assert!(result.is_err());
assert!(fetches.is_empty());
}
}