use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FResult};
use std::time::{Duration, Instant};
use brane_ast::locations::Location;
use brane_shr::formatters::BlockFormatter;
use log::debug;
use num_traits::AsPrimitive;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use reqwest::{Response, StatusCode};
use specifications::address::Address;
pub const DEFAULT_DOMAIN_REGISTRY_CACHE_TIMEOUT: u64 = 6 * 3600;
#[derive(Debug)]
pub enum DomainRegistryCacheError {
RequestSend { kind: &'static str, url: String, err: reqwest::Error },
ResponseDownload { url: String, err: reqwest::Error },
ResponseFailure { url: String, code: StatusCode, response: Option<String> },
ResponseParse { url: String, raw: String, err: serde_json::Error },
UnknownLocation { addr: Address, loc: Location },
}
impl Display for DomainRegistryCacheError {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
use DomainRegistryCacheError::*;
match self {
RequestSend { kind, url, .. } => write!(f, "Failed to send {kind}-request to '{url}'"),
ResponseDownload { url, .. } => write!(f, "Failed to download body of response from '{url}'"),
ResponseFailure { url, code, response } => write!(
f,
"Request to '{}' failed with {} ({}){}",
url,
code.as_u16(),
code.canonical_reason().unwrap_or("???"),
if let Some(response) = response { format!("\n\nResponse:\n{}\n", BlockFormatter::new(response)) } else { String::new() }
),
ResponseParse { url, raw, .. } => {
write!(f, "Failed to parse response from '{}' as valid JSON\n\nResponse:\n{}\n", url, BlockFormatter::new(raw))
},
UnknownLocation { addr, loc } => write!(f, "Unknown location '{loc}' to registry '{addr}'"),
}
}
}
impl Error for DomainRegistryCacheError {
fn source(&self) -> Option<&(dyn 'static + Error)> {
use DomainRegistryCacheError::*;
match self {
RequestSend { err, .. } => Some(err),
ResponseDownload { err, .. } => Some(err),
ResponseFailure { .. } => None,
ResponseParse { err, .. } => Some(err),
UnknownLocation { .. } => None,
}
}
}
#[derive(Debug)]
pub struct DomainRegistryCache {
timeout: u64,
api: Address,
data: RwLock<HashMap<Location, (Address, Instant)>>,
}
impl DomainRegistryCache {
#[inline]
pub fn new(api_address: impl Into<Address>) -> Self {
Self { timeout: DEFAULT_DOMAIN_REGISTRY_CACHE_TIMEOUT, api: api_address.into(), data: RwLock::new(HashMap::with_capacity(16)) }
}
#[inline]
pub fn with_timeout(timeout: impl AsPrimitive<u64>, api_address: impl Into<Address>) -> Self {
Self { timeout: timeout.as_(), api: api_address.into(), data: RwLock::new(HashMap::with_capacity(16)) }
}
pub async fn get<'s>(&'s self, location: &'_ Location) -> Result<Address, DomainRegistryCacheError> {
debug!("Resolving location '{}' in registry '{}'", location, self.api);
{
let lock: RwLockReadGuard<HashMap<String, (Address, Instant)>> = self.data.read();
if let Some((addr, cached)) = lock.get(location) {
if cached.elapsed() < Duration::from_secs(self.timeout) {
debug!("Found valid cached entry for '{location}', returning address '{addr}'");
return Ok(addr.clone());
}
debug!("Found expired cached entry for '{location}', fetching new address...");
} else {
debug!("No cached entry for '{location}' found, fetching new address...");
}
}
let url: String = format!("{}/infra/registries", self.api);
debug!("Sending GET-request to '{url}'...");
let res: Response = match reqwest::get(&url).await {
Ok(res) => res,
Err(err) => return Err(DomainRegistryCacheError::RequestSend { kind: "GET", url, err }),
};
if !res.status().is_success() {
return Err(DomainRegistryCacheError::ResponseFailure { url, code: res.status(), response: res.text().await.ok() });
}
debug!("Parsing response from registry...");
let res: String = match res.text().await {
Ok(res) => res,
Err(err) => return Err(DomainRegistryCacheError::ResponseDownload { url, err }),
};
let res: HashMap<String, Address> = match serde_json::from_str(&res) {
Ok(res) => res,
Err(err) => return Err(DomainRegistryCacheError::ResponseParse { url, raw: res, err }),
};
debug!("Registry listed '{}' locations", res.len());
let now: Instant = Instant::now();
let mut lock: RwLockWriteGuard<HashMap<String, (Address, Instant)>> = self.data.write();
lock.extend(res.into_iter().map(|(name, addr)| (name, (addr, now))));
match lock.get(location) {
Some((addr, _)) => {
debug!("Returning newly cached address '{addr}'");
Ok(addr.clone())
},
None => Err(DomainRegistryCacheError::UnknownLocation { addr: self.api.clone(), loc: location.clone() }),
}
}
}