Skip to main content

ic_timers/snapshot/
identity.rs

1//! Bounded, deterministically ordered timer identity.
2
3use std::{fmt, sync::Arc};
4use thiserror::Error;
5
6/// Maximum encoded length of one timer identity component.
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_IDENTITY_COMPONENT_BYTES: usize = 64;
11
12/// The identity component whose validation failed.
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub enum TimerIdentityField {
15    /// Scheduling owner, such as a framework or application.
16    Owner,
17    /// Functional area within the owner.
18    Subsystem,
19    /// Stable logical timer name.
20    Name,
21}
22
23impl fmt::Display for TimerIdentityField {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        formatter.write_str(match self {
26            Self::Owner => "owner",
27            Self::Subsystem => "subsystem",
28            Self::Name => "name",
29        })
30    }
31}
32
33/// Invalid structured timer identity.
34#[non_exhaustive]
35#[derive(Clone, Debug, Eq, Error, PartialEq)]
36pub enum TimerIdentityError {
37    /// One identity component was empty.
38    #[error("invalid timer {field}: identity component must not be empty")]
39    Empty {
40        /// Identity component that failed validation.
41        field: TimerIdentityField,
42    },
43    /// One identity component exceeded the UTF-8 byte bound.
44    #[error(
45        "invalid timer {field}: identity component is {actual_bytes} bytes; maximum is {max_bytes}"
46    )]
47    TooLong {
48        /// Identity component that failed validation.
49        field: TimerIdentityField,
50        /// Encoded length of the rejected value.
51        actual_bytes: usize,
52        /// Maximum supported encoded length.
53        max_bytes: usize,
54    },
55    /// One identity component had leading or trailing whitespace.
56    #[error("invalid timer {field}: identity component has leading or trailing whitespace")]
57    SurroundingWhitespace {
58        /// Identity component that failed validation.
59        field: TimerIdentityField,
60    },
61    /// One identity component contained a control character.
62    #[error(
63        "invalid timer {field}: identity component contains control character {character:?} at byte {byte_index}"
64    )]
65    ControlCharacter {
66        /// Identity component that failed validation.
67        field: TimerIdentityField,
68        /// UTF-8 byte offset of the rejected character.
69        byte_index: usize,
70        /// Rejected control character.
71        character: char,
72    },
73}
74
75/// Stable, low-cardinality identity for one logical canister timer.
76///
77/// Derived ordering is lexicographic by owner, subsystem, then name.
78/// Clones share immutable component storage and retain value-based equality.
79#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub struct TimerIdentity {
81    components: Arc<IdentityComponents>,
82}
83
84#[derive(Eq, Hash, Ord, PartialEq, PartialOrd)]
85struct IdentityComponents {
86    owner: Box<str>,
87    subsystem: Box<str>,
88    name: Box<str>,
89}
90
91impl TimerIdentity {
92    /// Validate all three identity components.
93    pub fn try_new(
94        owner: impl AsRef<str>,
95        subsystem: impl AsRef<str>,
96        name: impl AsRef<str>,
97    ) -> Result<Self, TimerIdentityError> {
98        Ok(Self {
99            components: Arc::new(IdentityComponents {
100                owner: validate_component(TimerIdentityField::Owner, owner)?,
101                subsystem: validate_component(TimerIdentityField::Subsystem, subsystem)?,
102                name: validate_component(TimerIdentityField::Name, name)?,
103            }),
104        })
105    }
106
107    /// Return the scheduling owner.
108    #[must_use]
109    pub fn owner(&self) -> &str {
110        &self.components.owner
111    }
112
113    /// Return the functional subsystem.
114    #[must_use]
115    pub fn subsystem(&self) -> &str {
116        &self.components.subsystem
117    }
118
119    /// Return the stable logical timer name.
120    #[must_use]
121    pub fn name(&self) -> &str {
122        &self.components.name
123    }
124}
125
126impl fmt::Debug for TimerIdentity {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter
129            .debug_struct("TimerIdentity")
130            .field("owner", &self.owner())
131            .field("subsystem", &self.subsystem())
132            .field("name", &self.name())
133            .finish()
134    }
135}
136
137fn validate_component(
138    field: TimerIdentityField,
139    value: impl AsRef<str>,
140) -> Result<Box<str>, TimerIdentityError> {
141    let value = value.as_ref();
142    if value.is_empty() {
143        return Err(TimerIdentityError::Empty { field });
144    }
145    if value.len() > MAX_TIMER_IDENTITY_COMPONENT_BYTES {
146        return Err(TimerIdentityError::TooLong {
147            field,
148            actual_bytes: value.len(),
149            max_bytes: MAX_TIMER_IDENTITY_COMPONENT_BYTES,
150        });
151    }
152    if value.trim() != value {
153        return Err(TimerIdentityError::SurroundingWhitespace { field });
154    }
155    if let Some((byte_index, character)) = value.char_indices().find(|(_, ch)| ch.is_control()) {
156        return Err(TimerIdentityError::ControlCharacter {
157            field,
158            byte_index,
159            character,
160        });
161    }
162
163    Ok(value.into())
164}