use crate::error::Result;
use crate::utils::{CacheEntry, CacheMode, FetchGuards};
use std::collections::HashMap;
use tokio::sync::RwLock;
pub(crate) struct DomainCache<V> {
mode: CacheMode,
entries: RwLock<HashMap<String, CacheEntry<V>>>,
guards: FetchGuards<String>,
}
impl<V: Clone> DomainCache<V> {
pub(crate) fn new(mode: CacheMode) -> Self {
Self {
mode,
entries: RwLock::new(HashMap::new()),
guards: FetchGuards::default(),
}
}
pub(crate) async fn get_or_try<F, Fut>(&self, key: String, f: F) -> Result<V>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<V>>,
{
if !self.mode.enabled() {
return f().await;
}
if let Some(entry) = self.entries.read().await.get(&key)
&& entry.is_fresh(self.mode)
{
return Ok(entry.value.clone());
}
self.guards
.dedup(key.clone(), || async {
if let Some(entry) = self.entries.read().await.get(&key)
&& entry.is_fresh(self.mode)
{
return Ok(entry.value.clone());
}
let value = f().await?;
crate::utils::cache_insert(
&mut *self.entries.write().await,
key.clone(),
value.clone(),
self.mode,
crate::utils::EVICTION_THRESHOLD,
);
Ok(value)
})
.await
}
}
macro_rules! domain_handle {
(
$(#[$meta:meta])*
pub struct $name:ident { $field:ident, $accessor:ident }
cache: $val:ty, chart
$(, extra: { $( $(#[$ecfg:meta])* $ecache:ident: $eval:ty ),+ $(,)? } )?
) => {
$(#[$meta])*
pub struct $name {
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
chart_cache: crate::domains::DomainCache<crate::models::chart::Chart>,
$($( $(#[$ecfg])* $ecache: crate::domains::DomainCache<$eval>, )+)?
}
impl $name {
pub(crate) fn with_providers(
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field,
providers,
cache: crate::domains::DomainCache::new(crate::utils::CacheMode::default()),
chart_cache: crate::domains::DomainCache::new(crate::utils::CacheMode::default()),
$($(
$(#[$ecfg])*
$ecache: crate::domains::DomainCache::new(crate::utils::CacheMode::default()),
)+)?
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
let mode = crate::utils::CacheMode::Ttl(ttl);
self.cache = crate::domains::DomainCache::new(mode);
self.chart_cache = crate::domains::DomainCache::new(mode);
$($(
$(#[$ecfg])*
{ self.$ecache = crate::domains::DomainCache::new(mode); }
)+)?
self
}
pub fn cache_forever(mut self) -> Self {
let mode = crate::utils::CacheMode::Lifetime;
self.cache = crate::domains::DomainCache::new(mode);
self.chart_cache = crate::domains::DomainCache::new(mode);
$($(
$(#[$ecfg])*
{ self.$ecache = crate::domains::DomainCache::new(mode); }
)+)?
self
}
pub fn no_cache(mut self) -> Self {
let mode = crate::utils::CacheMode::Off;
self.cache = crate::domains::DomainCache::new(mode);
self.chart_cache = crate::domains::DomainCache::new(mode);
$($(
$(#[$ecfg])*
{ self.$ecache = crate::domains::DomainCache::new(mode); }
)+)?
self
}
pub fn $accessor(&self) -> &str {
&self.$field
}
}
};
(
$(#[$meta:meta])*
pub struct $name:ident {
$(#[$meta1:meta])* $field1:ident, $accessor1:ident,
$(#[$meta2:meta])* $field2:ident, $accessor2:ident $(,)?
} cache: $val:ty, chart
) => {
$(#[$meta])*
pub struct $name {
$field1: std::sync::Arc<str>,
$field2: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
chart_cache: crate::domains::DomainCache<crate::models::chart::Chart>,
}
impl $name {
pub(crate) fn with_providers(
$field1: std::sync::Arc<str>,
$field2: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field1,
$field2,
providers,
cache: crate::domains::DomainCache::new(crate::utils::CacheMode::default()),
chart_cache: crate::domains::DomainCache::new(crate::utils::CacheMode::default()),
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
let mode = crate::utils::CacheMode::Ttl(ttl);
self.cache = crate::domains::DomainCache::new(mode);
self.chart_cache = crate::domains::DomainCache::new(mode);
self
}
pub fn cache_forever(mut self) -> Self {
let mode = crate::utils::CacheMode::Lifetime;
self.cache = crate::domains::DomainCache::new(mode);
self.chart_cache = crate::domains::DomainCache::new(mode);
self
}
pub fn no_cache(mut self) -> Self {
let mode = crate::utils::CacheMode::Off;
self.cache = crate::domains::DomainCache::new(mode);
self.chart_cache = crate::domains::DomainCache::new(mode);
self
}
$(#[$meta1])*
pub fn $accessor1(&self) -> &str {
&self.$field1
}
$(#[$meta2])*
pub fn $accessor2(&self) -> &str {
&self.$field2
}
}
};
($(#[$meta:meta])* pub struct $name:ident { $field:ident, $accessor:ident } cache: $val:ty) => {
$(#[$meta])*
pub struct $name {
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
}
impl $name {
pub(crate) fn with_providers(
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field,
providers,
cache: crate::domains::DomainCache::new(crate::utils::CacheMode::default()),
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
self.cache = crate::domains::DomainCache::new(crate::utils::CacheMode::Ttl(ttl));
self
}
pub fn cache_forever(mut self) -> Self {
self.cache = crate::domains::DomainCache::new(crate::utils::CacheMode::Lifetime);
self
}
pub fn no_cache(mut self) -> Self {
self.cache = crate::domains::DomainCache::new(crate::utils::CacheMode::Off);
self
}
pub fn $accessor(&self) -> &str {
&self.$field
}
}
};
(
$(#[$meta:meta])*
pub struct $name:ident
$(cfg: $cfg:meta,)?
caches: { $($cache:ident : $val:ty),+ $(,)? }
) => {
$(#[$meta])*
$(#[cfg($cfg)])?
pub struct $name {
providers: std::sync::Arc<crate::providers::ProviderSet>,
$($cache: crate::domains::DomainCache<$val>,)+
}
$(#[cfg($cfg)])?
impl $name {
pub(crate) fn with_providers(
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
providers,
$($cache: crate::domains::DomainCache::new(
crate::utils::CacheMode::default(),
),)+
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
let mode = crate::utils::CacheMode::Ttl(ttl);
$(self.$cache = crate::domains::DomainCache::new(mode);)+
self
}
pub fn cache_forever(mut self) -> Self {
let mode = crate::utils::CacheMode::Lifetime;
$(self.$cache = crate::domains::DomainCache::new(mode);)+
self
}
pub fn no_cache(mut self) -> Self {
let mode = crate::utils::CacheMode::Off;
$(self.$cache = crate::domains::DomainCache::new(mode);)+
self
}
}
};
}
#[allow(unused_macros)]
macro_rules! dispatch_via {
(
$self:expr, $cap:ident, $acc:ident, $op:ident, $fetch:ident,
[$($owned:ident),* $(,)?] $(, $arg:expr)* $(,)?
) => {{
$self
.providers
.fetch(crate::providers::Capability::$cap, move |p| {
$(let $owned = $owned.clone();)*
let p = p.clone();
async move {
p.$acc()
.ok_or_else(|| p.not_supported(crate::providers::Operation::$op))?
.$fetch($($arg),*)
.await
}
})
.await
}};
}
macro_rules! fetch_via {
($self:expr, $field:ident, $cap:ident, $acc:ident, $op:ident, $fetch:ident, $ret:ty) => {
fetch_via!(cache: cache, $self, $field, $cap, $acc, $op, $fetch, $ret)
};
(cache: $store:ident, $self:expr, $field:ident, $cap:ident, $acc:ident, $op:ident, $fetch:ident, $ret:ty) => {{
let __sym = $self.$field.clone();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.$store
.get_or_try(String::new(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __s = __sym.clone();
let p = p.clone();
async move {
p.$acc()
.ok_or_else(|| p.not_supported(crate::providers::Operation::$op))?
.$fetch(&__s)
.await
}
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_via_two {
($self:expr, $field1:ident, $field2:ident, $cap:ident, $acc:ident, $op:ident, $fetch:ident, $ret:ty) => {{
let __a = $self.$field1.clone();
let __b = $self.$field2.clone();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(String::new(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __x = __a.clone();
let __y = __b.clone();
let p = p.clone();
async move {
p.$acc()
.ok_or_else(|| p.not_supported(crate::providers::Operation::$op))?
.$fetch(&__x, &__y)
.await
}
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_via_with {
($self:expr, $field:ident, $cap:ident, $acc:ident, $op:ident, $fetch:ident, $arg:expr, $ret:ty) => {{
let __sym = $self.$field.clone();
let __arg = ($arg).to_string();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(__arg.clone(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __s = __sym.clone();
let __a = __arg.clone();
let p = p.clone();
async move {
p.$acc()
.ok_or_else(|| p.not_supported(crate::providers::Operation::$op))?
.$fetch(&__s, &__a)
.await
}
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_chart_via {
($self:expr, $sym:expr, $interval:expr, $range:expr) => {{
let __sym: String = $sym;
let __interval = $interval;
let __range = $range;
let __providers = std::sync::Arc::clone(&$self.providers);
let __key = format!("{}:{}:{}", __sym, __interval, __range);
$self
.chart_cache
.get_or_try(__key, move || async move {
__providers
.fetch(crate::providers::Capability::CHART, move |p| {
let __s = __sym.clone();
let p = p.clone();
async move {
p.as_chart()
.ok_or_else(|| p.not_supported(crate::providers::Operation::Chart))?
.fetch_chart(&__s, __interval, __range)
.await
}
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! impl_chartable_analytics {
($name:ident, $cal:expr) => {
impl $name {
#[cfg(feature = "indicators")]
pub async fn indicators(
&self,
interval: crate::Interval,
range: crate::TimeRange,
) -> crate::error::Result<crate::indicators::IndicatorsSummary> {
let chart = self.chart(interval, range).await?;
Ok(crate::indicators::summary::calculate_indicators(
&chart.candles,
))
}
#[cfg(feature = "indicators")]
pub async fn indicator(
&self,
indicator: crate::indicators::Indicator,
interval: crate::Interval,
range: crate::TimeRange,
) -> crate::error::Result<crate::indicators::IndicatorResult> {
let chart = self.chart(interval, range).await?;
Ok(crate::indicators::compute_indicator(indicator, &chart)?)
}
#[cfg(feature = "risk")]
pub async fn risk(
&self,
interval: crate::Interval,
range: crate::TimeRange,
) -> crate::error::Result<crate::risk::RiskSummary> {
let chart = self.chart(interval, range).await?;
Ok(crate::risk::compute_risk_summary_with_periods(
&chart.candles,
None,
crate::risk::periods_per_year(interval, $cal),
))
}
}
};
}
pub(crate) mod commodities;
pub(crate) mod crypto;
pub(crate) mod discovery;
pub(crate) mod economic;
pub(crate) mod filings;
pub(crate) mod forex;
pub(crate) mod futures;
pub(crate) mod indices;
pub(crate) mod market;
pub(crate) mod snapshot;
pub use commodities::Commodity;
pub use crypto::CryptoCoin;
pub use discovery::Discovery;
pub use economic::{EconomicCatalog, EconomicIndicator};
pub use filings::Filings;
pub use forex::ForexPair;
pub use futures::FuturesContract;
pub use indices::Index;
pub use market::Market;
pub use market::MarketCalendar;
pub use snapshot::Snapshot;
#[cfg(test)]
mod tests {
use super::*;
use crate::error::FinanceError;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
fn err() -> FinanceError {
FinanceError::NoProviderAvailable {
operation: crate::providers::Capability::QUOTE,
candidates: Vec::new(),
}
}
#[tokio::test]
async fn default_caches_across_calls() {
let cache: DomainCache<u32> = DomainCache::new(CacheMode::default());
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(42)
})
.await
.unwrap();
assert_eq!(v, 42);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn off_fetches_every_call() {
let cache: DomainCache<u32> = DomainCache::new(CacheMode::Off);
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(42)
})
.await
.unwrap();
assert_eq!(v, 42);
}
assert_eq!(calls.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn ttl_caches_within_window() {
let cache: DomainCache<u32> = DomainCache::new(CacheMode::Ttl(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(7)
})
.await
.unwrap();
assert_eq!(v, 7);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn distinct_keys_cached_separately() {
let cache: DomainCache<String> = DomainCache::new(CacheMode::Ttl(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
for key in ["usd", "eur", "usd", "eur"] {
let c = calls.clone();
let owned = key.to_string();
let v = cache
.get_or_try(key.to_string(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<String, FinanceError>(owned)
})
.await
.unwrap();
assert_eq!(v, key);
}
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn errors_are_not_cached() {
let cache: DomainCache<u32> = DomainCache::new(CacheMode::Ttl(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
let c = calls.clone();
let first = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Err::<u32, FinanceError>(err())
})
.await;
assert!(first.is_err());
let c = calls.clone();
let second = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(5)
})
.await
.unwrap();
assert_eq!(second, 5);
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn concurrent_misses_dedup_to_one_fetch() {
let cache: Arc<DomainCache<u32>> =
Arc::new(DomainCache::new(CacheMode::Ttl(Duration::from_secs(60))));
let calls = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let cache = Arc::clone(&cache);
let c = calls.clone();
handles.push(tokio::spawn(async move {
cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(20)).await;
Ok::<u32, FinanceError>(1)
})
.await
.unwrap()
}));
}
for h in handles {
assert_eq!(h.await.unwrap(), 1);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn failed_fetches_still_dedup_for_waiters() {
let cache: Arc<DomainCache<u32>> =
Arc::new(DomainCache::new(CacheMode::Ttl(Duration::from_secs(60))));
let in_flight = Arc::new(AtomicUsize::new(0));
let overlap = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for i in 0..8 {
let cache = Arc::clone(&cache);
let in_flight = Arc::clone(&in_flight);
let overlap = Arc::clone(&overlap);
handles.push(tokio::spawn(async move {
cache
.get_or_try("k".to_string(), move || async move {
if in_flight.fetch_add(1, Ordering::SeqCst) > 0 {
overlap.fetch_add(1, Ordering::SeqCst);
}
tokio::time::sleep(Duration::from_millis(20)).await;
in_flight.fetch_sub(1, Ordering::SeqCst);
Err::<u32, FinanceError>(FinanceError::ApiError(format!("boom {i}")))
})
.await
}));
}
for h in handles {
assert!(h.await.unwrap().is_err());
}
assert_eq!(
overlap.load(Ordering::SeqCst),
0,
"fetches for one key overlapped; the dedup guard was dropped too early"
);
}
#[tokio::test]
async fn entries_are_bounded_under_lifetime_caching() {
let cache: DomainCache<u32> = DomainCache::new(CacheMode::Lifetime);
for i in 0..500u32 {
cache
.get_or_try(i.to_string(), || async move { Ok::<u32, FinanceError>(i) })
.await
.unwrap();
}
let len = cache.entries.read().await.len();
assert!(
len <= crate::utils::EVICTION_THRESHOLD,
"domain cache grew to {len} entries"
);
}
#[tokio::test]
async fn distinct_keys_do_not_serialize() {
let cache: Arc<DomainCache<u32>> = Arc::new(DomainCache::new(CacheMode::default()));
let mut handles = Vec::new();
for k in ["a", "b", "c", "d"] {
let cache = Arc::clone(&cache);
handles.push(tokio::spawn(async move {
cache
.get_or_try(k.to_string(), || async {
tokio::time::sleep(Duration::from_millis(80)).await;
Ok::<u32, FinanceError>(1)
})
.await
.unwrap()
}));
}
let start = tokio::time::Instant::now();
for h in handles {
assert_eq!(h.await.unwrap(), 1);
}
assert!(
start.elapsed() < Duration::from_millis(200),
"four distinct keys took {:?}; they are serializing",
start.elapsed()
);
}
}