use futures::{FutureExt, future::BoxFuture};
use std::{
collections::HashSet,
sync::{Arc, Mutex},
time::Duration,
};
use tracing::Level;
use crate::{
exec::time::{Instant, sleep},
rch::oneshot,
rtc::{CallDecision, CallGuard, ClientMonitor, Req, ReqEnum},
};
#[derive(Debug, Default)]
struct State {
failed_methods: HashSet<&'static str>,
window_start: Option<Instant>,
count: usize,
}
impl State {
fn record_failure(&mut self, window: Duration) {
match self.window_start {
Some(start) if start.elapsed() < window => self.count += 1,
_ => {
self.window_start = Some(Instant::now());
self.count = 1;
}
}
}
fn over_limit(&self, limit: usize, window: Duration) -> bool {
match self.window_start {
Some(start) if start.elapsed() < window => self.count > limit,
_ => false,
}
}
}
#[derive(Debug)]
pub struct IncompatibleServerMonitor {
log_level: Option<Level>,
limit: Option<usize>,
window: Duration,
state: Arc<Mutex<State>>,
}
impl IncompatibleServerMonitor {
pub const DEFAULT_LOG_LEVEL: Option<Level> = super::IncompatibleClientMonitor::DEFAULT_LOG_LEVEL;
pub const DEFAULT_LIMIT: Option<usize> = Some(super::IncompatibleClientMonitor::DEFAULT_LIMIT.unwrap() / 2);
pub const DEFAULT_WINDOW: Duration = super::IncompatibleClientMonitor::DEFAULT_WINDOW;
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn log_level(mut self, level: Option<Level>) -> Self {
self.log_level = level;
self
}
#[must_use]
pub fn limit(mut self, limit: Option<usize>) -> Self {
self.limit = limit;
self
}
#[must_use]
pub fn window(mut self, window: Duration) -> Self {
self.window = window;
self
}
}
impl Default for IncompatibleServerMonitor {
fn default() -> Self {
Self {
log_level: Self::DEFAULT_LOG_LEVEL,
limit: Self::DEFAULT_LIMIT,
window: Self::DEFAULT_WINDOW,
state: Arc::new(Mutex::new(State::default())),
}
}
}
impl<Value, Ref, RefMut> ClientMonitor<Value, Ref, RefMut> for IncompatibleServerMonitor
where
Value: ReqEnum,
Ref: ReqEnum,
RefMut: ReqEnum,
{
fn pre_call<'a>(&'a self, req: &'a Req<Value, Ref, RefMut>) -> BoxFuture<'a, CallDecision> {
let trait_name = Req::<Value, Ref, RefMut>::trait_name();
let method_name = req.method_name();
let throttle = match self.limit {
Some(limit) => {
let state = self.state.lock().unwrap();
state.failed_methods.contains(method_name) && state.over_limit(limit, self.window)
}
None => false,
};
let guard = IncompatibleServerGuard {
window: self.window,
state: self.state.clone(),
log_level: self.log_level,
trait_name,
method_name,
reply_failed: false,
};
async move {
if throttle {
let target = format!("{trait_name}::{method_name}");
log_at!(self.log_level, %target, "delaying previously failed call");
sleep(self.window).await;
}
CallDecision::Guard(Box::new(guard))
}
.boxed()
}
}
struct IncompatibleServerGuard {
window: Duration,
state: Arc<Mutex<State>>,
log_level: Option<Level>,
trait_name: &'static str,
method_name: &'static str,
reply_failed: bool,
}
impl CallGuard for IncompatibleServerGuard {
fn reply_failed(&mut self, error: &oneshot::RecvError) {
self.reply_failed = true;
let (trait_name, method_name) = (self.trait_name, self.method_name);
let target = format!("{trait_name}::{method_name}");
log_at!(self.log_level, %target, %error, "failed to call");
}
}
impl Drop for IncompatibleServerGuard {
fn drop(&mut self) {
let mut state = self.state.lock().unwrap();
if self.reply_failed {
state.failed_methods.insert(self.method_name);
state.record_failure(self.window);
} else {
state.failed_methods.remove(self.method_name);
}
}
}