use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use crate::fleet::{Histogram, HistogramSnapshot, LabeledCounter, MetricLabel};
use super::pick::RetryReason;
use super::route::RequestClass;
macro_rules! labels {
($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
impl MetricLabel for $name {
const ALL: &'static [$name] = &[$($name::$variant),+];
fn as_str(self) -> &'static str {
match self { $($name::$variant => $label),+ }
}
fn index(self) -> usize {
self as usize
}
}
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StatusClass {
Success,
Redirect,
ClientError,
ServerError,
}
labels!(StatusClass {
Success => "2xx",
Redirect => "3xx",
ClientError => "4xx",
ServerError => "5xx",
});
impl StatusClass {
pub fn of(status: u16) -> StatusClass {
match status {
300..=399 => StatusClass::Redirect,
400..=499 => StatusClass::ClientError,
500.. => StatusClass::ServerError,
_ => StatusClass::Success,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Rejection {
Connections,
InFlight,
BodyTooLarge,
Timeout,
}
labels!(Rejection {
Connections => "connections",
InFlight => "in_flight",
BodyTooLarge => "body_too_large",
Timeout => "timeout",
});
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BlockRegion {
Irreversible,
Reversible,
Unhinted,
}
labels!(BlockRegion {
Irreversible => "irreversible",
Reversible => "reversible",
Unhinted => "unhinted",
});
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AnswerRole {
Live,
Frozen,
None,
}
labels!(AnswerRole {
Live => "live",
Frozen => "frozen",
None => "none",
});
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PushResult {
Ok,
Error,
Ambiguous,
}
labels!(PushResult {
Ok => "ok",
Error => "error",
Ambiguous => "ambiguous",
});
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UpstreamResult {
Ok,
UnknownBlock,
NotFound,
Busy,
Error,
Connect,
Timeout,
}
labels!(UpstreamResult {
Ok => "ok",
UnknownBlock => "unknown_block",
NotFound => "not_found",
Busy => "busy",
Error => "error",
Connect => "connect",
Timeout => "timeout",
});
#[derive(Debug)]
pub struct ApiStats {
requests: Box<[[AtomicU64; 4]]>,
durations: Box<[Histogram]>,
in_flight: AtomicUsize,
pub rejected: LabeledCounter<Rejection>,
pub retries: LabeledCounter<RetryReason>,
block_requests: [[AtomicU64; 3]; 3],
pub pushes: LabeledCounter<PushResult>,
bound: AtomicBool,
}
impl Default for ApiStats {
fn default() -> Self {
ApiStats {
requests: RequestClass::ALL
.iter()
.map(|_| Default::default())
.collect(),
durations: RequestClass::ALL
.iter()
.map(|_| Histogram::default())
.collect(),
in_flight: AtomicUsize::new(0),
rejected: LabeledCounter::new(),
retries: LabeledCounter::new(),
block_requests: Default::default(),
pushes: LabeledCounter::new(),
bound: AtomicBool::new(false),
}
}
}
impl ApiStats {
pub fn request(&self, class: RequestClass, status: u16, elapsed: Duration) {
self.requests[class.index()][StatusClass::of(status).index()]
.fetch_add(1, Ordering::Relaxed);
self.durations[class.index()].observe(elapsed);
}
pub fn block_request(&self, region: BlockRegion, role: AnswerRole) {
self.block_requests[region.index()][role.index()].fetch_add(1, Ordering::Relaxed);
}
pub(super) fn enter(&self) {
self.in_flight.fetch_add(1, Ordering::Relaxed);
}
pub(super) fn leave(&self) {
self.in_flight.fetch_sub(1, Ordering::Relaxed);
}
pub(crate) fn set_bound(&self, bound: bool) {
self.bound.store(bound, Ordering::Relaxed);
}
}
impl ApiStats {
pub fn requests(&self, class: RequestClass, code: StatusClass) -> u64 {
self.requests[class.index()][code.index()].load(Ordering::Relaxed)
}
pub fn duration(&self, class: RequestClass) -> HistogramSnapshot {
self.durations[class.index()].snapshot()
}
pub fn block_requests(&self, region: BlockRegion, role: AnswerRole) -> u64 {
self.block_requests[region.index()][role.index()].load(Ordering::Relaxed)
}
pub fn in_flight(&self) -> usize {
self.in_flight.load(Ordering::Relaxed)
}
pub fn bound(&self) -> bool {
self.bound.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requests_count_by_class_and_status_class() {
let stats = ApiStats::default();
stats.request(RequestClass::Block, 200, Duration::from_millis(3));
stats.request(RequestClass::Block, 400, Duration::from_millis(3));
stats.request(RequestClass::Block, 503, Duration::from_millis(3));
stats.request(RequestClass::Denied, 404, Duration::from_micros(10));
assert_eq!(stats.requests(RequestClass::Block, StatusClass::Success), 1);
assert_eq!(
stats.requests(RequestClass::Block, StatusClass::ClientError),
1
);
assert_eq!(
stats.requests(RequestClass::Block, StatusClass::ServerError),
1
);
assert_eq!(
stats.requests(RequestClass::Denied, StatusClass::ClientError),
1
);
assert_eq!(stats.duration(RequestClass::Block).count, 3);
assert_eq!(StatusClass::of(101), StatusClass::Success);
assert_eq!(StatusClass::of(302), StatusClass::Redirect);
stats.block_request(BlockRegion::Unhinted, AnswerRole::Live);
assert_eq!(
stats.block_requests(BlockRegion::Unhinted, AnswerRole::Live),
1
);
assert_eq!(
stats.block_requests(BlockRegion::Irreversible, AnswerRole::None),
0
);
stats.enter();
assert_eq!(stats.in_flight(), 1);
stats.leave();
assert_eq!(stats.in_flight(), 0);
for labels in [
StatusClass::ALL
.iter()
.map(|l| l.as_str())
.collect::<Vec<_>>(),
Rejection::ALL.iter().map(|l| l.as_str()).collect(),
BlockRegion::ALL.iter().map(|l| l.as_str()).collect(),
AnswerRole::ALL.iter().map(|l| l.as_str()).collect(),
PushResult::ALL.iter().map(|l| l.as_str()).collect(),
UpstreamResult::ALL.iter().map(|l| l.as_str()).collect(),
] {
let mut unique = labels.clone();
unique.dedup();
assert_eq!(unique, labels);
}
assert_eq!(UpstreamResult::UnknownBlock.as_str(), "unknown_block");
assert_eq!(Rejection::BodyTooLarge.index(), 2);
}
}