Skip to main content

osproxy_core/
ids.rs

1//! Strongly-typed identifier newtypes.
2//!
3//! Bare `String`/`u64` identifiers must never cross an API boundary: they are
4//! trivially mixed up and they make traces ambiguous. Every identifier in the
5//! system is a distinct type so the compiler catches misuse and so telemetry
6//! can label each value precisely (`docs/08` §7, `docs/05`).
7
8use std::fmt;
9
10/// Defines a string-backed identifier newtype with `Display`, `From<String>`,
11/// `From<&str>`, and an `as_str` accessor. Keeps each id a distinct type while
12/// avoiding repetitive boilerplate.
13macro_rules! string_id {
14    ($(#[$meta:meta])* $name:ident) => {
15        $(#[$meta])*
16        #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
17        pub struct $name(String);
18
19        impl $name {
20            /// Borrows the underlying string.
21            #[must_use]
22            pub fn as_str(&self) -> &str {
23                &self.0
24            }
25
26            /// Consumes the id, returning the owned string.
27            #[must_use]
28            pub fn into_string(self) -> String {
29                self.0
30            }
31        }
32
33        impl From<String> for $name {
34            fn from(value: String) -> Self {
35                Self(value)
36            }
37        }
38
39        impl From<&str> for $name {
40            fn from(value: &str) -> Self {
41                Self(value.to_owned())
42            }
43        }
44
45        impl fmt::Display for $name {
46            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47                f.write_str(&self.0)
48            }
49        }
50
51        // Identifiers are safe to render in telemetry (they are ids, not
52        // tenant *values*); a precise Debug aids the `/debug/explain` story.
53        impl fmt::Debug for $name {
54            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55                write!(f, concat!(stringify!($name), "({:?})"), self.0)
56            }
57        }
58    };
59}
60
61string_id! {
62    /// The tenancy/placement unit. Everything routes by this (`docs/03` §1).
63    PartitionId
64}
65
66string_id! {
67    /// Identifies a physical OpenSearch cluster.
68    ClusterId
69}
70
71string_id! {
72    /// A concrete (physical) OpenSearch index name.
73    IndexName
74}
75
76string_id! {
77    /// The authenticated principal's stable id. Never the raw token (`docs/05`).
78    PrincipalId
79}
80
81string_id! {
82    /// Correlates all telemetry for a single request (`docs/05` §6).
83    RequestId
84}
85
86string_id! {
87    /// The name of a document field. Used both for fields the proxy injects on
88    /// ingest and the fields it strips on read, so the two stay symmetric
89    /// (`docs/02` §2, `docs/03`). A name, never a value, safe in telemetry.
90    FieldName
91}
92
93/// The placement-table generation a routing decision was resolved against.
94///
95/// Monotonically increasing. Stamped on every routed write so the sink can
96/// reject a stale-epoch write for a migrating partition (`docs/06` §2).
97#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
98pub struct Epoch(u64);
99
100impl Epoch {
101    /// The initial epoch.
102    pub const ZERO: Self = Self(0);
103
104    /// Constructs an epoch from a raw generation number.
105    #[must_use]
106    pub fn new(generation: u64) -> Self {
107        Self(generation)
108    }
109
110    /// Returns the raw generation number.
111    #[must_use]
112    pub fn get(self) -> u64 {
113        self.0
114    }
115
116    /// Returns the next epoch. Saturates at `u64::MAX` rather than wrapping, so
117    /// monotonicity (relied on by migration cutover, `docs/06` INV-M2) can
118    /// never be violated by overflow.
119    #[must_use]
120    pub fn next(self) -> Self {
121        Self(self.0.saturating_add(1))
122    }
123}
124
125impl fmt::Display for Epoch {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "{}", self.0)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn string_id_roundtrips_through_str_and_string() {
137        let from_str = PartitionId::from("tenant-7");
138        let from_string = PartitionId::from("tenant-7".to_owned());
139        assert_eq!(from_str, from_string);
140        assert_eq!(from_str.as_str(), "tenant-7");
141        assert_eq!(from_str.clone().into_string(), "tenant-7");
142    }
143
144    #[test]
145    fn distinct_id_types_do_not_compare_but_display_plainly() {
146        let cluster = ClusterId::from("eu-1");
147        assert_eq!(cluster.to_string(), "eu-1");
148        // Debug is labelled so traces are unambiguous.
149        assert_eq!(format!("{cluster:?}"), r#"ClusterId("eu-1")"#);
150    }
151
152    #[test]
153    fn epoch_is_monotonic_and_saturates() {
154        assert_eq!(Epoch::ZERO.get(), 0);
155        assert_eq!(Epoch::ZERO.next(), Epoch::new(1));
156        assert!(Epoch::new(1) > Epoch::ZERO);
157        assert_eq!(Epoch::new(u64::MAX).next(), Epoch::new(u64::MAX));
158        // Display renders the bare generation number (used in trace attributes).
159        assert_eq!(Epoch::new(42).to_string(), "42");
160    }
161}