Skip to main content

ic_timers/snapshot/
identity.rs

1//! Bounded, deterministically ordered timer identity.
2
3use std::{fmt, ops::Deref};
4use thiserror::Error;
5
6/// Maximum encoded length of one timer identity label.
7///
8/// The limit is measured in UTF-8 bytes so storage and metrics cardinality do
9/// not depend on Unicode scalar width.
10pub const MAX_TIMER_LABEL_BYTES: usize = 64;
11
12/// One bounded component of a timer identity.
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct TimerLabel(Box<str>);
15
16impl TimerLabel {
17    /// Validate and copy a timer label.
18    pub fn new(value: impl AsRef<str>) -> Result<Self, TimerLabelError> {
19        let value = value.as_ref();
20        if value.is_empty() {
21            return Err(TimerLabelError::Empty);
22        }
23        if value.len() > MAX_TIMER_LABEL_BYTES {
24            return Err(TimerLabelError::TooLong {
25                actual_bytes: value.len(),
26                max_bytes: MAX_TIMER_LABEL_BYTES,
27            });
28        }
29        if value.trim() != value {
30            return Err(TimerLabelError::SurroundingWhitespace);
31        }
32        if let Some((byte_index, character)) = value.char_indices().find(|(_, ch)| ch.is_control())
33        {
34            return Err(TimerLabelError::ControlCharacter {
35                byte_index,
36                character,
37            });
38        }
39
40        Ok(Self(value.into()))
41    }
42
43    /// Borrow the exact validated label.
44    #[must_use]
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50impl AsRef<str> for TimerLabel {
51    fn as_ref(&self) -> &str {
52        self.as_str()
53    }
54}
55
56impl Deref for TimerLabel {
57    type Target = str;
58
59    fn deref(&self) -> &Self::Target {
60        self.as_str()
61    }
62}
63
64impl fmt::Display for TimerLabel {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        formatter.write_str(self.as_str())
67    }
68}
69
70impl TryFrom<&str> for TimerLabel {
71    type Error = TimerLabelError;
72
73    fn try_from(value: &str) -> Result<Self, Self::Error> {
74        Self::new(value)
75    }
76}
77
78impl TryFrom<String> for TimerLabel {
79    type Error = TimerLabelError;
80
81    fn try_from(value: String) -> Result<Self, Self::Error> {
82        Self::new(value)
83    }
84}
85
86/// Invalid timer identity label.
87#[non_exhaustive]
88#[derive(Clone, Debug, Eq, Error, PartialEq)]
89pub enum TimerLabelError {
90    /// Labels must not be empty.
91    #[error("timer identity label must not be empty")]
92    Empty,
93    /// Labels must fit the documented UTF-8 byte bound.
94    #[error("timer identity label is {actual_bytes} bytes; maximum is {max_bytes}")]
95    TooLong {
96        /// Encoded length of the rejected value.
97        actual_bytes: usize,
98        /// Maximum supported encoded length.
99        max_bytes: usize,
100    },
101    /// Leading and trailing whitespace makes metric labels hard to distinguish.
102    #[error("timer identity label must not have leading or trailing whitespace")]
103    SurroundingWhitespace,
104    /// Control characters are not portable operator labels.
105    #[error("timer identity label contains control character {character:?} at byte {byte_index}")]
106    ControlCharacter {
107        /// UTF-8 byte offset of the rejected character.
108        byte_index: usize,
109        /// Rejected control character.
110        character: char,
111    },
112}
113
114/// The identity component whose validation failed.
115#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
116pub enum TimerIdentityField {
117    /// Scheduling owner, such as a framework or application.
118    Owner,
119    /// Functional area within the owner.
120    Subsystem,
121    /// Stable logical timer name.
122    Name,
123}
124
125impl fmt::Display for TimerIdentityField {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter.write_str(match self {
128            Self::Owner => "owner",
129            Self::Subsystem => "subsystem",
130            Self::Name => "name",
131        })
132    }
133}
134
135/// Invalid structured timer identity.
136#[non_exhaustive]
137#[derive(Clone, Debug, Eq, Error, PartialEq)]
138#[error("invalid timer {field}: {source}")]
139pub struct TimerIdentityError {
140    /// Identity component that failed validation.
141    pub field: TimerIdentityField,
142    /// Label validation error.
143    #[source]
144    pub source: TimerLabelError,
145}
146
147/// Stable, low-cardinality identity for one logical canister timer.
148///
149/// Derived ordering is lexicographic by owner, subsystem, then name.
150#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
151pub struct TimerIdentity {
152    owner: TimerLabel,
153    subsystem: TimerLabel,
154    name: TimerLabel,
155}
156
157impl TimerIdentity {
158    /// Construct an identity from already validated labels.
159    #[must_use]
160    pub const fn new(owner: TimerLabel, subsystem: TimerLabel, name: TimerLabel) -> Self {
161        Self {
162            owner,
163            subsystem,
164            name,
165        }
166    }
167
168    /// Validate all three identity components.
169    pub fn try_new(
170        owner: impl AsRef<str>,
171        subsystem: impl AsRef<str>,
172        name: impl AsRef<str>,
173    ) -> Result<Self, TimerIdentityError> {
174        Ok(Self::new(
175            validate_component(TimerIdentityField::Owner, owner)?,
176            validate_component(TimerIdentityField::Subsystem, subsystem)?,
177            validate_component(TimerIdentityField::Name, name)?,
178        ))
179    }
180
181    /// Return the scheduling owner.
182    #[must_use]
183    pub const fn owner(&self) -> &TimerLabel {
184        &self.owner
185    }
186
187    /// Return the functional subsystem.
188    #[must_use]
189    pub const fn subsystem(&self) -> &TimerLabel {
190        &self.subsystem
191    }
192
193    /// Return the stable logical timer name.
194    #[must_use]
195    pub const fn name(&self) -> &TimerLabel {
196        &self.name
197    }
198}
199
200fn validate_component(
201    field: TimerIdentityField,
202    value: impl AsRef<str>,
203) -> Result<TimerLabel, TimerIdentityError> {
204    TimerLabel::new(value).map_err(|source| TimerIdentityError { field, source })
205}