use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use digdigdig3::core::types::ExchangeId;
use thiserror::Error;
use tokio::sync::Mutex;
use tokio::time::Instant;
use crate::series::Kind;
use crate::station::StationInner;
use crate::subscription::MultiplexRef;
use crate::{Station, StationError, SubscribeReport, SubscriptionSet};
#[derive(Clone, Debug)]
pub struct ConsumerQuota {
pub max_active_subs: Option<u32>,
pub max_rest_per_window: Option<u32>,
pub rest_window: Duration,
pub whitelist: Option<Arc<ConsumerWhitelist>>,
}
impl Default for ConsumerQuota {
fn default() -> Self {
Self {
max_active_subs: None,
max_rest_per_window: None,
rest_window: Duration::from_secs(60),
whitelist: None,
}
}
}
impl ConsumerQuota {
pub fn unlimited() -> Self {
Self::default()
}
pub fn max_active_subs(mut self, n: u32) -> Self {
self.max_active_subs = Some(n);
self
}
pub fn max_rest(mut self, per_window: u32, window: Duration) -> Self {
self.max_rest_per_window = Some(per_window);
self.rest_window = window;
self
}
pub fn whitelist(mut self, wl: ConsumerWhitelist) -> Self {
self.whitelist = Some(Arc::new(wl));
self
}
}
#[derive(Debug, Default)]
pub struct ConsumerWhitelist {
pub exchanges: HashSet<ExchangeId>,
pub kinds: HashSet<Kind>,
pub symbols: Option<HashSet<String>>,
}
impl ConsumerWhitelist {
pub fn new() -> Self {
Self::default()
}
pub fn allow_exchange(mut self, e: ExchangeId) -> Self {
self.exchanges.insert(e);
self
}
pub fn allow_kind(mut self, k: Kind) -> Self {
self.kinds.insert(k);
self
}
pub fn allow_symbol(mut self, s: impl Into<String>) -> Self {
self.symbols.get_or_insert_with(HashSet::new).insert(s.into());
self
}
pub(crate) fn check(
&self,
exchange: ExchangeId,
kind: &Kind,
symbol: &str,
) -> Result<(), String> {
if !self.exchanges.is_empty() && !self.exchanges.contains(&exchange) {
return Err(format!("exchange {exchange:?} not whitelisted"));
}
if !self.kinds.is_empty() && !self.kinds.contains(kind) {
return Err(format!("kind {kind:?} not whitelisted"));
}
if let Some(syms) = &self.symbols {
if !syms.contains(symbol) {
return Err(format!("symbol {symbol} not whitelisted"));
}
}
Ok(())
}
}
#[derive(Debug, Error)]
pub enum QuotaError {
#[error("subscription quota exceeded: have {have}, cap {cap}")]
SubsCapExceeded { have: u32, cap: u32 },
#[error("REST rate limit: {remaining_ms}ms until next token")]
RestRateLimit { remaining_ms: u64 },
#[error("not in whitelist: {0}")]
NotInWhitelist(String),
#[error(transparent)]
Inner(#[from] StationError),
}
pub(crate) struct TokenBucket {
capacity: u32,
available: u32,
refill_window: Duration,
last_refill: Instant,
}
impl TokenBucket {
pub(crate) fn new(capacity: u32, window: Duration) -> Self {
Self {
capacity,
available: capacity,
refill_window: window,
last_refill: Instant::now(),
}
}
pub(crate) fn try_consume(&mut self) -> Result<(), u64> {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill);
if elapsed >= self.refill_window {
self.available = self.capacity;
self.last_refill = now;
}
if self.available > 0 {
self.available -= 1;
Ok(())
} else {
let wait = self.refill_window.saturating_sub(elapsed);
Err(wait.as_millis() as u64)
}
}
pub(crate) fn available(&self) -> u32 {
self.available
}
}
pub struct ConsumerHandle {
pub(crate) station: Arc<StationInner>,
pub(crate) quota: ConsumerQuota,
pub(crate) rest_bucket: Arc<Mutex<Option<TokenBucket>>>,
pub(crate) refs: Mutex<(u32, Vec<MultiplexRef>)>,
}
impl ConsumerHandle {
pub async fn active_sub_count(&self) -> u32 {
self.refs.lock().await.0
}
pub async fn rest_tokens_available(&self) -> u32 {
match self.rest_bucket.lock().await.as_ref() {
Some(b) => b.available(),
None => u32::MAX,
}
}
pub async fn subscribe(
&self,
set: SubscriptionSet,
) -> Result<SubscribeReport, QuotaError> {
let n_new: u32 = set
.entries
.iter()
.map(|e| e.streams.len() as u32)
.sum();
if let Some(wl) = &self.quota.whitelist {
for entry in &set.entries {
for s in &entry.streams {
let kind = s.to_kind();
if let Err(msg) = wl.check(entry.exchange, &kind, &entry.symbol) {
return Err(QuotaError::NotInWhitelist(msg));
}
}
}
}
let mut guard = self.refs.lock().await;
let current = guard.0;
if let Some(cap) = self.quota.max_active_subs {
if current.saturating_add(n_new) > cap {
return Err(QuotaError::SubsCapExceeded {
have: current.saturating_add(n_new),
cap,
});
}
}
let station = Station {
inner: self.station.clone(),
};
let report = station.subscribe(set).await?;
let ok_count = report.ok.len() as u32;
let report = report.take_refs_into(&mut guard.1);
guard.0 = guard.0.saturating_add(ok_count);
Ok(report)
}
pub async fn rest_gate(&self) -> Result<Station, QuotaError> {
let mut bucket = self.rest_bucket.lock().await;
if let Some(b) = bucket.as_mut() {
b.try_consume()
.map_err(|remaining_ms| QuotaError::RestRateLimit { remaining_ms })?;
}
Ok(Station { inner: self.station.clone() })
}
}