use std::any::Any;
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use crate::http::{Method, StatusCode};
use async_trait::async_trait;
use url::Url;
use crate::error::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperationInfo {
pub service: Cow<'static, str>,
pub operation: Cow<'static, str>,
pub resource_type: Cow<'static, str>,
pub is_mutation: bool,
pub resource_id: Option<i64>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RequestInfo {
pub method: Method,
pub url: Url,
pub attempt: u32,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct RequestResult<'a> {
pub status: Option<StatusCode>,
pub duration: Duration,
pub error: Option<&'a Error>,
pub from_cache: bool,
pub retryable: bool,
pub retry_after: Option<u64>,
}
pub type OperationState = Option<Box<dyn Any + Send>>;
#[async_trait]
pub trait Hooks: Send + Sync {
async fn on_operation_gate(&self, _op: &OperationInfo) -> Result<(), Error> {
Ok(())
}
fn on_operation_start(&self, _op: &OperationInfo) -> OperationState {
None
}
fn on_operation_end(
&self,
_op: &OperationInfo,
_state: OperationState,
_outcome: Result<(), &Error>,
_duration: Duration,
) {
}
fn on_request_start(&self, _info: &RequestInfo) {}
fn on_request_end(&self, _info: &RequestInfo, _result: &RequestResult<'_>) {}
fn on_retry(&self, _info: &RequestInfo, _next_attempt: u32, _cause: &Error) {}
fn is_noop(&self) -> bool {
false
}
}
#[async_trait]
impl<H: Hooks + ?Sized> Hooks for Arc<H> {
async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
(**self).on_operation_gate(op).await
}
fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
(**self).on_operation_start(op)
}
fn on_operation_end(
&self,
op: &OperationInfo,
state: OperationState,
outcome: Result<(), &Error>,
duration: Duration,
) {
(**self).on_operation_end(op, state, outcome, duration);
}
fn on_request_start(&self, info: &RequestInfo) {
(**self).on_request_start(info);
}
fn on_request_end(&self, info: &RequestInfo, result: &RequestResult<'_>) {
(**self).on_request_end(info, result);
}
fn on_retry(&self, info: &RequestInfo, next_attempt: u32, cause: &Error) {
(**self).on_retry(info, next_attempt, cause);
}
fn is_noop(&self) -> bool {
(**self).is_noop()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopHooks;
impl Hooks for NoopHooks {
fn is_noop(&self) -> bool {
true
}
}
pub struct ChainHooks {
hooks: Vec<Arc<dyn Hooks>>,
}
impl ChainHooks {
pub fn of(hooks: Vec<Arc<dyn Hooks>>) -> Arc<dyn Hooks> {
let mut installed: Vec<Arc<dyn Hooks>> =
hooks.into_iter().filter(|hook| !hook.is_noop()).collect();
if installed.is_empty() {
Arc::new(NoopHooks)
} else if installed.len() == 1 {
installed.remove(0)
} else {
Arc::new(ChainHooks { hooks: installed })
}
}
}
#[async_trait]
impl Hooks for ChainHooks {
async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
for hook in &self.hooks {
hook.on_operation_gate(op).await?;
}
Ok(())
}
fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
let states: Vec<OperationState> = self
.hooks
.iter()
.map(|hook| hook.on_operation_start(op))
.collect();
Some(Box::new(states))
}
fn on_operation_end(
&self,
op: &OperationInfo,
state: OperationState,
outcome: Result<(), &Error>,
duration: Duration,
) {
let mut states = member_states(state);
for hook in self.hooks.iter().rev() {
hook.on_operation_end(op, states.pop().flatten(), outcome, duration);
}
}
fn on_request_start(&self, info: &RequestInfo) {
for hook in &self.hooks {
hook.on_request_start(info);
}
}
fn on_request_end(&self, info: &RequestInfo, result: &RequestResult<'_>) {
for hook in self.hooks.iter().rev() {
hook.on_request_end(info, result);
}
}
fn on_retry(&self, info: &RequestInfo, next_attempt: u32, cause: &Error) {
for hook in &self.hooks {
hook.on_retry(info, next_attempt, cause);
}
}
}
fn member_states(state: OperationState) -> Vec<OperationState> {
match state.and_then(|state| state.downcast::<Vec<OperationState>>().ok()) {
Some(states) => *states,
None => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use crate::ErrorCode;
#[tokio::test]
async fn every_noop_callback_is_safe_to_call() {
let hooks = NoopHooks;
let op = operation_info();
let info = request_info();
hooks.on_operation_gate(&op).await.unwrap();
let state = hooks.on_operation_start(&op);
assert!(state.is_none());
hooks.on_request_start(&info);
hooks.on_request_end(&info, &request_result());
hooks.on_retry(&info, 2, &Error::usage("nothing"));
hooks.on_operation_end(&op, state, Ok(()), Duration::from_secs(1));
assert!(hooks.is_noop());
}
#[test]
fn a_chain_runs_forwards_and_unwinds_backwards() {
let log = Log::new();
let chain = ChainHooks::of(vec![log.recorder("first"), log.recorder("second")]);
let op = operation_info();
let state = chain.on_operation_start(&op);
chain.on_request_start(&request_info());
chain.on_request_end(&request_info(), &request_result());
chain.on_retry(&request_info(), 2, &Error::usage("nothing"));
chain.on_operation_end(&op, state, Ok(()), Duration::from_secs(1));
assert_eq!(
log.entries(),
[
"first: start Svc.Do",
"second: start Svc.Do",
"first: request start 1",
"second: request start 1",
"second: request end 200",
"first: request end 200",
"first: retry 2",
"second: retry 2",
"second: end Svc.Do carrying second",
"first: end Svc.Do carrying first",
]
);
}
#[test]
fn a_chain_of_one_is_that_hook() {
let log = Log::new();
let recorder = log.recorder("only");
let chain = ChainHooks::of(vec![recorder.clone(), Arc::new(NoopHooks)]);
assert!(Arc::ptr_eq(&chain, &recorder));
}
#[test]
fn a_chain_of_nothing_but_noops_is_a_noop() {
assert!(ChainHooks::of(vec![Arc::new(NoopHooks), Arc::new(NoopHooks)]).is_noop());
assert!(ChainHooks::of(Vec::new()).is_noop());
}
#[tokio::test]
async fn a_chain_answers_the_first_refusal() {
let log = Log::new();
let chain = ChainHooks::of(vec![
log.recorder("first"),
Arc::new(Refusing),
log.recorder("third"),
]);
let refused = chain
.on_operation_gate(&operation_info())
.await
.unwrap_err();
assert_eq!(refused.code(), ErrorCode::Usage);
assert_eq!(refused.message(), "blocked");
assert_eq!(log.entries(), ["first: gate Svc.Do"]);
}
struct Log {
entries: Arc<Mutex<Vec<String>>>,
}
impl Log {
fn new() -> Log {
Log {
entries: Arc::new(Mutex::new(Vec::new())),
}
}
fn recorder(&self, name: &'static str) -> Arc<dyn Hooks> {
Arc::new(Recorder {
name,
entries: self.entries.clone(),
})
}
fn entries(&self) -> Vec<String> {
self.entries.lock().unwrap().clone()
}
}
struct Recorder {
name: &'static str,
entries: Arc<Mutex<Vec<String>>>,
}
impl Recorder {
fn record(&self, event: &str) {
self.entries
.lock()
.unwrap()
.push(format!("{}: {event}", self.name));
}
}
#[async_trait]
impl Hooks for Recorder {
async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
self.record(&format!("gate {}.{}", op.service, op.operation));
Ok(())
}
fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
self.record(&format!("start {}.{}", op.service, op.operation));
Some(Box::new(self.name.to_string()))
}
fn on_operation_end(
&self,
op: &OperationInfo,
state: OperationState,
_outcome: Result<(), &Error>,
_duration: Duration,
) {
let carried = match state.and_then(|state| state.downcast::<String>().ok()) {
Some(name) => *name,
None => "nothing".to_string(),
};
self.record(&format!(
"end {}.{} carrying {carried}",
op.service, op.operation
));
}
fn on_request_start(&self, info: &RequestInfo) {
self.record(&format!("request start {}", info.attempt));
}
fn on_request_end(&self, _info: &RequestInfo, result: &RequestResult<'_>) {
self.record(&format!("request end {}", result.status.unwrap().as_u16()));
}
fn on_retry(&self, _info: &RequestInfo, next_attempt: u32, _cause: &Error) {
self.record(&format!("retry {next_attempt}"));
}
}
struct Refusing;
#[async_trait]
impl Hooks for Refusing {
async fn on_operation_gate(&self, _op: &OperationInfo) -> Result<(), Error> {
Err(Error::usage("blocked"))
}
}
fn operation_info() -> OperationInfo {
OperationInfo {
service: Cow::Borrowed("Svc"),
operation: Cow::Borrowed("Do"),
resource_type: Cow::Borrowed("thing"),
is_mutation: false,
resource_id: None,
}
}
fn request_info() -> RequestInfo {
RequestInfo {
method: Method::GET,
url: Url::parse("https://app.hey.example/boxes.json").unwrap(),
attempt: 1,
}
}
fn request_result() -> RequestResult<'static> {
RequestResult {
status: Some(StatusCode::OK),
duration: Duration::from_millis(3),
error: None,
from_cache: false,
retryable: false,
retry_after: None,
}
}
}