1use std::any::Any;
5use std::borrow::Cow;
6use std::sync::Arc;
7use std::time::Duration;
8
9use crate::http::{Method, StatusCode};
10use async_trait::async_trait;
11use url::Url;
12
13use crate::error::Error;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct OperationInfo {
23 pub service: Cow<'static, str>,
25 pub operation: Cow<'static, str>,
27 pub resource_type: Cow<'static, str>,
29 pub is_mutation: bool,
31 pub resource_id: Option<i64>,
33}
34
35#[derive(Debug, Clone)]
38#[non_exhaustive]
39pub struct RequestInfo {
40 pub method: Method,
42 pub url: Url,
44 pub attempt: u32,
46}
47
48#[derive(Debug)]
50#[non_exhaustive]
51pub struct RequestResult<'a> {
52 pub status: Option<StatusCode>,
54 pub duration: Duration,
56 pub error: Option<&'a Error>,
58 pub from_cache: bool,
61 pub retryable: bool,
63 pub retry_after: Option<u64>,
65}
66
67pub type OperationState = Option<Box<dyn Any + Send>>;
69
70#[async_trait]
105pub trait Hooks: Send + Sync {
106 async fn on_operation_gate(&self, _op: &OperationInfo) -> Result<(), Error> {
115 Ok(())
116 }
117
118 fn on_operation_start(&self, _op: &OperationInfo) -> OperationState {
121 None
122 }
123
124 fn on_operation_end(
127 &self,
128 _op: &OperationInfo,
129 _state: OperationState,
130 _outcome: Result<(), &Error>,
131 _duration: Duration,
132 ) {
133 }
134
135 fn on_request_start(&self, _info: &RequestInfo) {}
137
138 fn on_request_end(&self, _info: &RequestInfo, _result: &RequestResult<'_>) {}
140
141 fn on_retry(&self, _info: &RequestInfo, _next_attempt: u32, _cause: &Error) {}
144
145 fn is_noop(&self) -> bool {
148 false
149 }
150}
151
152#[async_trait]
153impl<H: Hooks + ?Sized> Hooks for Arc<H> {
154 async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
155 (**self).on_operation_gate(op).await
156 }
157
158 fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
159 (**self).on_operation_start(op)
160 }
161
162 fn on_operation_end(
163 &self,
164 op: &OperationInfo,
165 state: OperationState,
166 outcome: Result<(), &Error>,
167 duration: Duration,
168 ) {
169 (**self).on_operation_end(op, state, outcome, duration);
170 }
171
172 fn on_request_start(&self, info: &RequestInfo) {
173 (**self).on_request_start(info);
174 }
175
176 fn on_request_end(&self, info: &RequestInfo, result: &RequestResult<'_>) {
177 (**self).on_request_end(info, result);
178 }
179
180 fn on_retry(&self, info: &RequestInfo, next_attempt: u32, cause: &Error) {
181 (**self).on_retry(info, next_attempt, cause);
182 }
183
184 fn is_noop(&self) -> bool {
185 (**self).is_noop()
186 }
187}
188
189#[derive(Debug, Clone, Copy, Default)]
192pub struct NoopHooks;
193
194impl Hooks for NoopHooks {
195 fn is_noop(&self) -> bool {
196 true
197 }
198}
199
200pub struct ChainHooks {
203 hooks: Vec<Arc<dyn Hooks>>,
204}
205
206impl ChainHooks {
207 pub fn of(hooks: Vec<Arc<dyn Hooks>>) -> Arc<dyn Hooks> {
211 let mut installed: Vec<Arc<dyn Hooks>> =
212 hooks.into_iter().filter(|hook| !hook.is_noop()).collect();
213 if installed.is_empty() {
214 Arc::new(NoopHooks)
215 } else if installed.len() == 1 {
216 installed.remove(0)
217 } else {
218 Arc::new(ChainHooks { hooks: installed })
219 }
220 }
221}
222
223#[async_trait]
224impl Hooks for ChainHooks {
225 async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
229 for hook in &self.hooks {
230 hook.on_operation_gate(op).await?;
231 }
232 Ok(())
233 }
234
235 fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
238 let states: Vec<OperationState> = self
239 .hooks
240 .iter()
241 .map(|hook| hook.on_operation_start(op))
242 .collect();
243 Some(Box::new(states))
244 }
245
246 fn on_operation_end(
247 &self,
248 op: &OperationInfo,
249 state: OperationState,
250 outcome: Result<(), &Error>,
251 duration: Duration,
252 ) {
253 let mut states = member_states(state);
254 for hook in self.hooks.iter().rev() {
255 hook.on_operation_end(op, states.pop().flatten(), outcome, duration);
256 }
257 }
258
259 fn on_request_start(&self, info: &RequestInfo) {
260 for hook in &self.hooks {
261 hook.on_request_start(info);
262 }
263 }
264
265 fn on_request_end(&self, info: &RequestInfo, result: &RequestResult<'_>) {
266 for hook in self.hooks.iter().rev() {
267 hook.on_request_end(info, result);
268 }
269 }
270
271 fn on_retry(&self, info: &RequestInfo, next_attempt: u32, cause: &Error) {
272 for hook in &self.hooks {
273 hook.on_retry(info, next_attempt, cause);
274 }
275 }
276}
277
278fn member_states(state: OperationState) -> Vec<OperationState> {
279 match state.and_then(|state| state.downcast::<Vec<OperationState>>().ok()) {
280 Some(states) => *states,
281 None => Vec::new(),
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use std::sync::Mutex;
288
289 use super::*;
290 use crate::ErrorCode;
291
292 #[tokio::test]
293 async fn every_noop_callback_is_safe_to_call() {
294 let hooks = NoopHooks;
295 let op = operation_info();
296 let info = request_info();
297
298 hooks.on_operation_gate(&op).await.unwrap();
299 let state = hooks.on_operation_start(&op);
300 assert!(state.is_none());
301 hooks.on_request_start(&info);
302 hooks.on_request_end(&info, &request_result());
303 hooks.on_retry(&info, 2, &Error::usage("nothing"));
304 hooks.on_operation_end(&op, state, Ok(()), Duration::from_secs(1));
305
306 assert!(hooks.is_noop());
307 }
308
309 #[test]
310 fn a_chain_runs_forwards_and_unwinds_backwards() {
311 let log = Log::new();
312 let chain = ChainHooks::of(vec![log.recorder("first"), log.recorder("second")]);
313 let op = operation_info();
314
315 let state = chain.on_operation_start(&op);
316 chain.on_request_start(&request_info());
317 chain.on_request_end(&request_info(), &request_result());
318 chain.on_retry(&request_info(), 2, &Error::usage("nothing"));
319 chain.on_operation_end(&op, state, Ok(()), Duration::from_secs(1));
320
321 assert_eq!(
322 log.entries(),
323 [
324 "first: start Svc.Do",
325 "second: start Svc.Do",
326 "first: request start 1",
327 "second: request start 1",
328 "second: request end 200",
329 "first: request end 200",
330 "first: retry 2",
331 "second: retry 2",
332 "second: end Svc.Do carrying second",
333 "first: end Svc.Do carrying first",
334 ]
335 );
336 }
337
338 #[test]
339 fn a_chain_of_one_is_that_hook() {
340 let log = Log::new();
341 let recorder = log.recorder("only");
342
343 let chain = ChainHooks::of(vec![recorder.clone(), Arc::new(NoopHooks)]);
344
345 assert!(Arc::ptr_eq(&chain, &recorder));
346 }
347
348 #[test]
349 fn a_chain_of_nothing_but_noops_is_a_noop() {
350 assert!(ChainHooks::of(vec![Arc::new(NoopHooks), Arc::new(NoopHooks)]).is_noop());
351 assert!(ChainHooks::of(Vec::new()).is_noop());
352 }
353
354 #[tokio::test]
355 async fn a_chain_answers_the_first_refusal() {
356 let log = Log::new();
357 let chain = ChainHooks::of(vec![
358 log.recorder("first"),
359 Arc::new(Refusing),
360 log.recorder("third"),
361 ]);
362
363 let refused = chain
364 .on_operation_gate(&operation_info())
365 .await
366 .unwrap_err();
367
368 assert_eq!(refused.code(), ErrorCode::Usage);
369 assert_eq!(refused.message(), "blocked");
370 assert_eq!(log.entries(), ["first: gate Svc.Do"]);
371 }
372
373 struct Log {
375 entries: Arc<Mutex<Vec<String>>>,
376 }
377
378 impl Log {
379 fn new() -> Log {
380 Log {
381 entries: Arc::new(Mutex::new(Vec::new())),
382 }
383 }
384
385 fn recorder(&self, name: &'static str) -> Arc<dyn Hooks> {
386 Arc::new(Recorder {
387 name,
388 entries: self.entries.clone(),
389 })
390 }
391
392 fn entries(&self) -> Vec<String> {
393 self.entries.lock().unwrap().clone()
394 }
395 }
396
397 struct Recorder {
398 name: &'static str,
399 entries: Arc<Mutex<Vec<String>>>,
400 }
401
402 impl Recorder {
403 fn record(&self, event: &str) {
404 self.entries
405 .lock()
406 .unwrap()
407 .push(format!("{}: {event}", self.name));
408 }
409 }
410
411 #[async_trait]
412 impl Hooks for Recorder {
413 async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
414 self.record(&format!("gate {}.{}", op.service, op.operation));
415 Ok(())
416 }
417
418 fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
419 self.record(&format!("start {}.{}", op.service, op.operation));
420 Some(Box::new(self.name.to_string()))
421 }
422
423 fn on_operation_end(
424 &self,
425 op: &OperationInfo,
426 state: OperationState,
427 _outcome: Result<(), &Error>,
428 _duration: Duration,
429 ) {
430 let carried = match state.and_then(|state| state.downcast::<String>().ok()) {
431 Some(name) => *name,
432 None => "nothing".to_string(),
433 };
434 self.record(&format!(
435 "end {}.{} carrying {carried}",
436 op.service, op.operation
437 ));
438 }
439
440 fn on_request_start(&self, info: &RequestInfo) {
441 self.record(&format!("request start {}", info.attempt));
442 }
443
444 fn on_request_end(&self, _info: &RequestInfo, result: &RequestResult<'_>) {
445 self.record(&format!("request end {}", result.status.unwrap().as_u16()));
446 }
447
448 fn on_retry(&self, _info: &RequestInfo, next_attempt: u32, _cause: &Error) {
449 self.record(&format!("retry {next_attempt}"));
450 }
451 }
452
453 struct Refusing;
454
455 #[async_trait]
456 impl Hooks for Refusing {
457 async fn on_operation_gate(&self, _op: &OperationInfo) -> Result<(), Error> {
458 Err(Error::usage("blocked"))
459 }
460 }
461
462 fn operation_info() -> OperationInfo {
463 OperationInfo {
464 service: Cow::Borrowed("Svc"),
465 operation: Cow::Borrowed("Do"),
466 resource_type: Cow::Borrowed("thing"),
467 is_mutation: false,
468 resource_id: None,
469 }
470 }
471
472 fn request_info() -> RequestInfo {
473 RequestInfo {
474 method: Method::GET,
475 url: Url::parse("https://app.hey.example/boxes.json").unwrap(),
476 attempt: 1,
477 }
478 }
479
480 fn request_result() -> RequestResult<'static> {
481 RequestResult {
482 status: Some(StatusCode::OK),
483 duration: Duration::from_millis(3),
484 error: None,
485 from_cache: false,
486 retryable: false,
487 retry_after: None,
488 }
489 }
490}