Skip to main content

request_shadow/
shadower.rs

1//! [`Shadower`] — the main type. Fires primary + shadow legs concurrently and
2//! returns the primary response plus an optional divergence.
3
4use std::sync::Arc;
5
6use tokio::time::timeout;
7
8use crate::backend::{Backend, ResponseRecord};
9use crate::config::ShadowConfig;
10use crate::divergence::Divergence;
11use crate::error::ShadowError;
12use crate::log::{DivergenceEntry, DivergenceLog};
13
14/// Result of a shadow call. The client *always* sees `primary` — even if the
15/// shadow leg failed or timed out, only telemetry is affected.
16#[derive(Debug)]
17pub struct ShadowOutcome {
18    /// The primary response. This is what the calling code should return.
19    pub primary: ResponseRecord,
20    /// The shadow response, if it succeeded inside the configured timeout.
21    pub shadow: Option<ResponseRecord>,
22    /// Structured diff. `None` when bodies/status/headers all match (modulo
23    /// the config's ignore list) or when the shadow didn't run.
24    pub divergence: Option<Divergence>,
25    /// True when the request would have been mirrored but the sampler said no.
26    pub skipped_by_sampler: bool,
27    /// True when the shadow leg timed out / errored.
28    pub shadow_failed: Option<String>,
29}
30
31/// The shadower. Cheap to clone — both backends are `Arc`-shaped.
32#[derive(Clone)]
33pub struct Shadower {
34    primary: Arc<dyn Backend>,
35    shadow: Arc<dyn Backend>,
36    config: ShadowConfig,
37    log: Arc<DivergenceLog>,
38}
39
40impl Shadower {
41    /// Build a shadower with the default ring-buffer-backed [`DivergenceLog`].
42    pub fn new(primary: Arc<dyn Backend>, shadow: Arc<dyn Backend>, config: ShadowConfig) -> Self {
43        Self {
44            primary,
45            shadow,
46            config,
47            log: Arc::new(DivergenceLog::default()),
48        }
49    }
50
51    /// Override the log (e.g. with a custom capacity).
52    #[must_use]
53    pub fn with_log(mut self, log: Arc<DivergenceLog>) -> Self {
54        self.log = log;
55        self
56    }
57
58    /// Snapshot the divergence log.
59    pub fn divergences(&self) -> Vec<DivergenceEntry> {
60        self.log.snapshot()
61    }
62
63    /// Issue a call. Both legs run concurrently. The shadow's deadline is the
64    /// `ShadowConfig::shadow_timeout`; expiry never blocks the primary.
65    pub async fn call(&self, input: &[u8]) -> Result<ShadowOutcome, ShadowError> {
66        let should_shadow = self.config.should_shadow(input);
67
68        if !should_shadow {
69            let primary = self.primary.call(input).await?;
70            return Ok(ShadowOutcome {
71                primary,
72                shadow: None,
73                divergence: None,
74                skipped_by_sampler: true,
75                shadow_failed: None,
76            });
77        }
78
79        let primary_fut = self.primary.call(input);
80        let shadow_fut = timeout(self.config.shadow_timeout, self.shadow.call(input));
81
82        let (primary_res, shadow_res) = tokio::join!(primary_fut, shadow_fut);
83        let primary = primary_res?;
84
85        let (shadow, shadow_failed) = match shadow_res {
86            Ok(Ok(resp)) => (Some(resp), None),
87            Ok(Err(err)) => (None, Some(err.to_string())),
88            Err(_) => (None, Some("timeout".to_string())),
89        };
90
91        let divergence = match &shadow {
92            Some(s) => Divergence::compare(&primary, s, &self.config),
93            None => None,
94        };
95
96        if let Some(d) = &divergence {
97            self.log.push(DivergenceEntry {
98                key: input.to_vec(),
99                divergence: d.clone(),
100            });
101        }
102
103        Ok(ShadowOutcome {
104            primary,
105            shadow,
106            divergence,
107            skipped_by_sampler: false,
108            shadow_failed,
109        })
110    }
111
112    /// Internal — count entries (used by tests).
113    pub fn divergence_count(&self) -> usize {
114        self.log.len()
115    }
116}