Skip to main content

request_shadow/
config.rs

1//! Shadow configuration.
2
3use std::time::Duration;
4
5use sha2::{Digest, Sha256};
6
7/// Diff field a caller wants to ignore.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum IgnoreField {
10    /// Don't flag a status-code difference.
11    Status,
12    /// Don't flag a header difference.
13    Headers,
14    /// Don't flag a body difference.
15    Body,
16}
17
18/// Knobs the shadower needs.
19#[derive(Clone, Debug)]
20pub struct ShadowConfig {
21    /// Percentage of requests that get mirrored. `0..=100`. Default 100.
22    sample_rate: u32,
23    /// Max time the shadow leg is allowed. After this we drop it and flag a
24    /// `ShadowTimeout` on the outcome — never blocks the primary.
25    pub shadow_timeout: Duration,
26    /// Fields to skip in the divergence check.
27    pub ignore: Vec<IgnoreField>,
28}
29
30impl ShadowConfig {
31    /// Mirror every request, default timeout 2s.
32    pub fn full_sample() -> Self {
33        Self {
34            sample_rate: 100,
35            shadow_timeout: Duration::from_secs(2),
36            ignore: Vec::new(),
37        }
38    }
39
40    /// Set sampling rate as a percentage (`0..=100`). Values >100 are clamped.
41    #[must_use]
42    pub fn sample_rate(mut self, percent: u32) -> Self {
43        self.sample_rate = percent.min(100);
44        self
45    }
46
47    /// Override the shadow timeout.
48    #[must_use]
49    pub fn shadow_timeout(mut self, d: Duration) -> Self {
50        self.shadow_timeout = d;
51        self
52    }
53
54    /// Add a field to skip in the divergence diff.
55    #[must_use]
56    pub fn ignore(mut self, field: IgnoreField) -> Self {
57        self.ignore.push(field);
58        self
59    }
60
61    /// Configured sample rate (0..=100).
62    pub fn sample_rate_percent(&self) -> u32 {
63        self.sample_rate
64    }
65
66    /// Decide whether this request should be mirrored. Bucketing is sticky on
67    /// the `key` — the same key always gets the same yes/no for a given
68    /// `sample_rate`. SHA-256 (mod 100) is cheap and avoids an RNG dep.
69    pub fn should_shadow(&self, key: &[u8]) -> bool {
70        if self.sample_rate >= 100 {
71            return true;
72        }
73        if self.sample_rate == 0 {
74            return false;
75        }
76        let mut hasher = Sha256::new();
77        hasher.update(key);
78        let digest = hasher.finalize();
79        let bucket = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]) % 100;
80        bucket < self.sample_rate
81    }
82}
83
84impl Default for ShadowConfig {
85    fn default() -> Self {
86        Self::full_sample()
87    }
88}