use std::future::Future;
use std::pin::Pin;
use chrono::Utc;
use redis::AsyncCommands;
use redis::aio::ConnectionManager;
use crate::error::StateError;
use crate::tenant::TenantContext;
pub trait TokenMeter: Send + Sync {
fn current<'a>(
&'a self,
tenant: &'a TenantContext,
) -> Pin<Box<dyn Future<Output = Result<u64, StateError>> + Send + 'a>>;
fn add<'a>(
&'a self,
tenant: &'a TenantContext,
tokens: u64,
) -> Pin<Box<dyn Future<Output = Result<(), StateError>> + Send + 'a>>;
}
pub struct RedisTokenMeter {
manager: ConnectionManager,
}
impl RedisTokenMeter {
pub fn new(manager: ConnectionManager) -> Self {
Self { manager }
}
fn key(tenant: &TenantContext) -> String {
let day = Utc::now().format("%Y%m%d").to_string();
format!("{}:cost:tokens:{day}", tenant.key_prefix())
}
}
impl TokenMeter for RedisTokenMeter {
fn current<'a>(
&'a self,
tenant: &'a TenantContext,
) -> Pin<Box<dyn Future<Output = Result<u64, StateError>> + Send + 'a>> {
Box::pin(async move {
let key = Self::key(tenant);
let mut conn = self.manager.clone();
let value: Option<u64> = conn
.get(&key)
.await
.map_err(|e| StateError::Redis(format!("get: {e}")))?;
Ok(value.unwrap_or(0))
})
}
fn add<'a>(
&'a self,
tenant: &'a TenantContext,
tokens: u64,
) -> Pin<Box<dyn Future<Output = Result<(), StateError>> + Send + 'a>> {
Box::pin(async move {
let key = Self::key(tenant);
let mut conn = self.manager.clone();
let _: i64 = conn
.incr(&key, tokens as i64)
.await
.map_err(|e| StateError::Redis(format!("incrby: {e}")))?;
let _: bool = conn
.expire(&key, 86_400)
.await
.map_err(|e| StateError::Redis(format!("expire: {e}")))?;
Ok(())
})
}
}
#[cfg(feature = "test-mock")]
pub struct MockTokenMeter {
pub current_value: std::sync::Mutex<u64>,
}
#[cfg(feature = "test-mock")]
impl MockTokenMeter {
pub fn new(current: u64) -> Self {
Self {
current_value: std::sync::Mutex::new(current),
}
}
}
#[cfg(feature = "test-mock")]
impl TokenMeter for MockTokenMeter {
#[allow(clippy::expect_used)] fn current<'a>(
&'a self,
_t: &'a TenantContext,
) -> Pin<Box<dyn Future<Output = Result<u64, StateError>> + Send + 'a>> {
let v = *self
.current_value
.lock()
.expect("mock token meter poisoned");
Box::pin(async move { Ok(v) })
}
#[allow(clippy::expect_used)] fn add<'a>(
&'a self,
_t: &'a TenantContext,
tokens: u64,
) -> Pin<Box<dyn Future<Output = Result<(), StateError>> + Send + 'a>> {
{
let mut v = self
.current_value
.lock()
.expect("mock token meter poisoned");
*v += tokens;
}
Box::pin(async move { Ok(()) })
}
}