#![allow(dead_code)]
use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex,
},
time::{Duration, Instant},
};
use tracing::field::{Field, Visit};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
pub const DEFAULT_USER: &str = "0x0000000000000000000000000000000000000001";
pub const DEFAULT_CONCURRENCY: usize = 4;
pub const MAX_PAGE_LIMIT: u32 = 50;
const MAX_WARN_SAMPLES: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarnKind {
Throttle,
Other,
}
pub fn classify(message: &str) -> WarnKind {
if message.contains("Retriable status 429") {
WarnKind::Throttle
} else {
WarnKind::Other
}
}
#[derive(Debug)]
pub struct ThrottleObserver {
start: Instant,
throttles: AtomicU64,
other_warnings: AtomicU64,
first_throttle_micros: AtomicU64,
samples: Mutex<Vec<String>>,
}
impl ThrottleObserver {
pub fn new(start: Instant) -> Self {
Self {
start,
throttles: AtomicU64::new(0),
other_warnings: AtomicU64::new(0),
first_throttle_micros: AtomicU64::new(u64::MAX),
samples: Mutex::new(Vec::new()),
}
}
pub fn record(&self, message: &str) {
match classify(message) {
WarnKind::Throttle => {
self.throttles.fetch_add(1, Ordering::Relaxed);
let elapsed = self.start.elapsed().as_micros() as u64;
let _ = self.first_throttle_micros.compare_exchange(
u64::MAX,
elapsed,
Ordering::Relaxed,
Ordering::Relaxed,
);
}
WarnKind::Other => {
self.other_warnings.fetch_add(1, Ordering::Relaxed);
}
}
if let Ok(mut samples) = self.samples.lock() {
if samples.len() < MAX_WARN_SAMPLES {
samples.push(message.to_owned());
}
}
}
pub fn throttled(&self) -> bool {
self.throttle_count() > 0
}
pub fn throttle_count(&self) -> u64 {
self.throttles.load(Ordering::Relaxed)
}
pub fn other_warning_count(&self) -> u64 {
self.other_warnings.load(Ordering::Relaxed)
}
pub fn first_throttle_at(&self) -> Option<Duration> {
match self.first_throttle_micros.load(Ordering::Relaxed) {
u64::MAX => None,
micros => Some(Duration::from_micros(micros)),
}
}
pub fn warn_samples(&self) -> Vec<String> {
match self.samples.lock() {
Ok(samples) => samples.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
pub fn reset(&self) {
self.throttles.store(0, Ordering::Relaxed);
self.other_warnings.store(0, Ordering::Relaxed);
self.first_throttle_micros
.store(u64::MAX, Ordering::Relaxed);
if let Ok(mut samples) = self.samples.lock() {
samples.clear();
}
}
}
#[derive(Default)]
struct MessageVisitor(Option<String>);
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0 = Some(format!("{value:?}"));
}
}
}
struct ThrottleLayer(Arc<ThrottleObserver>);
impl<S: tracing::Subscriber> Layer<S> for ThrottleLayer {
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let metadata = event.metadata();
if !metadata.target().starts_with("polyoxide_core")
|| *metadata.level() != tracing::Level::WARN
{
return;
}
let mut visitor = MessageVisitor::default();
event.record(&mut visitor);
if let Some(message) = visitor.0 {
self.0.record(&message);
}
}
}
pub fn install_observer(start: Instant) -> Arc<ThrottleObserver> {
let observer = Arc::new(ThrottleObserver::new(start));
tracing_subscriber::registry()
.with(ThrottleLayer(Arc::clone(&observer)))
.init();
observer
}
pub fn percentile(sorted: &[Duration], p: f64) -> Duration {
if sorted.is_empty() {
return Duration::ZERO;
}
let last = sorted.len() - 1;
let rank = (p / 100.0 * last as f64).round() as usize;
sorted[rank.min(last)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_recognises_the_retry_loop_429() {
assert_eq!(
classify(
"Retriable status 429 Too Many Requests on /closed-positions, retry 1 after 0ms"
),
WarnKind::Throttle
);
}
#[test]
fn classify_does_not_count_425_as_throttling() {
assert_eq!(
classify("Retriable status 425 Too Early on /closed-positions, retry 1 after 500ms"),
WarnKind::Other
);
}
#[test]
fn observer_keeps_the_first_throttle_timestamp() {
let observer = ThrottleObserver::new(Instant::now());
observer.record("Retriable status 429 on /closed-positions, retry 1 after 500ms");
let first = observer
.first_throttle_at()
.expect("first throttle recorded");
observer.record("Retriable status 429 on /closed-positions, retry 2 after 1000ms");
assert_eq!(observer.throttle_count(), 2);
assert_eq!(
observer.first_throttle_at(),
Some(first),
"a later throttle must not overwrite the first timestamp"
);
}
#[test]
fn observer_separates_other_warnings_from_throttles() {
let observer = ThrottleObserver::new(Instant::now());
observer.record("Retriable status 425 Too Early on /trades, retry 1 after 500ms");
assert!(!observer.throttled(), "a 425 is not upstream rate limiting");
assert_eq!(observer.other_warning_count(), 1);
assert_eq!(observer.first_throttle_at(), None);
}
#[test]
fn reset_clears_every_field_so_trials_stay_independent() {
let observer = ThrottleObserver::new(Instant::now());
observer.record("Retriable status 429 on /closed-positions, retry 1 after 500ms");
observer.record("Retriable status 425 Too Early on /trades, retry 1 after 500ms");
observer.reset();
assert_eq!(observer.throttle_count(), 0);
assert_eq!(observer.other_warning_count(), 0);
assert_eq!(observer.first_throttle_at(), None);
assert!(observer.warn_samples().is_empty());
}
#[test]
fn percentile_of_empty_is_zero() {
assert_eq!(percentile(&[], 50.0), Duration::ZERO);
}
#[test]
fn percentile_picks_by_nearest_rank() {
let sorted: Vec<Duration> = (1..=100).map(Duration::from_millis).collect();
assert_eq!(percentile(&sorted, 50.0), Duration::from_millis(51));
assert_eq!(percentile(&sorted, 99.0), Duration::from_millis(99));
assert_eq!(percentile(&sorted, 100.0), Duration::from_millis(100));
}
}