Skip to main content

submilli_engine/runtime/
durable.rs

1//! Shared identities for durable execution.
2
3/// Identity of one run. Its effects and resource handles share this namespace.
4#[derive(
5    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
6)]
7#[serde(transparent)]
8pub struct RunId(pub String);
9
10impl From<String> for RunId {
11    fn from(value: String) -> Self {
12        Self(value)
13    }
14}
15impl From<&str> for RunId {
16    fn from(value: &str) -> Self {
17        Self(value.to_owned())
18    }
19}
20impl std::ops::Deref for RunId {
21    type Target = str;
22    fn deref(&self) -> &str {
23        self.as_str()
24    }
25}
26impl std::fmt::Display for RunId {
27    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        formatter.write_str(self.as_str())
29    }
30}
31impl RunId {
32    /// Allocate an identity once at admission; retries retain the original ID.
33    pub fn generate() -> std::io::Result<Self> {
34        let mut bytes = [0; 16];
35        getrandom::getrandom(&mut bytes)
36            .map_err(|error| std::io::Error::other(format!("run ID entropy: {error}")))?;
37        Ok(Self(
38            uuid::Builder::from_random_bytes(bytes)
39                .into_uuid()
40                .to_string(),
41        ))
42    }
43
44    pub fn as_str(&self) -> &str {
45        &self.0
46    }
47}
48
49impl std::borrow::Borrow<str> for RunId {
50    fn borrow(&self) -> &str {
51        self.as_str()
52    }
53}
54
55/// Identity of one logical call. Retries must reuse it; distinct calls must not.
56#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
57#[serde(transparent)]
58pub struct EffectId(pub String);
59impl From<String> for EffectId {
60    fn from(value: String) -> Self {
61        Self(value)
62    }
63}
64impl From<&str> for EffectId {
65    fn from(value: &str) -> Self {
66        Self(value.to_owned())
67    }
68}
69impl std::ops::Deref for EffectId {
70    type Target = str;
71    fn deref(&self) -> &str {
72        &self.0
73    }
74}
75impl std::fmt::Display for EffectId {
76    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        formatter.write_str(self.as_str())
78    }
79}
80
81impl EffectId {
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85
86    /// Temporary per-call identity until durable execution supplies stable IDs.
87    /// This stub is not replay-stable; backend retries must retain their ID.
88    pub fn stub() -> std::io::Result<Self> {
89        let mut bytes = [0; 16];
90        getrandom::getrandom(&mut bytes)
91            .map_err(|error| std::io::Error::other(format!("effect ID entropy: {error}")))?;
92        Ok(uuid::Builder::from_random_bytes(bytes)
93            .into_uuid()
94            .to_string()
95            .into())
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn generated_runs_have_distinct_uuid_identities() {
105        let first = RunId::generate().unwrap();
106        let second = RunId::generate().unwrap();
107        assert_ne!(first, second);
108        assert_eq!(
109            uuid::Uuid::parse_str(first.as_str())
110                .unwrap()
111                .get_version_num(),
112            4
113        );
114    }
115
116    #[test]
117    fn stored_identities_remain_plain_strings() {
118        for value in ["legacy-reservation", "00000000-0000-4000-8000-000000000001"] {
119            let id = RunId::from(value);
120            let encoded = serde_json::to_value(&id).unwrap();
121            assert_eq!(encoded, value);
122            assert_eq!(serde_json::from_value::<RunId>(encoded).unwrap(), id);
123        }
124    }
125}