Skip to main content

hydro_lang/location/
member_id.rs

1//! Typed and untyped identifiers for members of a [`Cluster`](super::Cluster).
2//!
3//! In Hydro, a [`Cluster`](super::Cluster) is a location that represents a group of
4//! identical processes. Each individual process within a cluster is identified by a
5//! [`MemberId`], which is parameterized by a tag type `Tag` to prevent accidentally
6//! mixing up member IDs from different clusters.
7//!
8//! [`TaglessMemberId`] is the underlying untyped representation, which carries the
9//! actual runtime identity (e.g. a raw numeric ID, a Docker container name, or a
10//! Maelstrom node ID) without any compile-time cluster tag.
11
12use std::fmt::{Debug, Display};
13use std::hash::Hash;
14use std::marker::PhantomData;
15
16use serde::{Deserialize, Serialize};
17
18/// An untyped identifier for a member of a cluster, without a compile-time tag
19/// distinguishing which cluster it belongs to.
20///
21/// The available variants depend on which runtime features are enabled. This enum
22/// is `#[non_exhaustive]` because new runtime backends may add additional variants.
23///
24/// In most user code, prefer [`MemberId<Tag>`] which carries a type-level tag to
25/// prevent mixing up members from different clusters.
26#[derive(Clone, Deserialize, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
27#[non_exhaustive] // Variants change based on features.
28pub enum TaglessMemberId {
29    /// A legacy numeric member ID, used with the `deploy_integration` / `embedded_runtime` feature.
30    #[cfg(any(
31        feature = "deploy",
32        feature = "deploy_integration",
33        feature = "embedded_runtime"
34    ))]
35    #[cfg_attr(
36        docsrs,
37        doc(cfg(any(
38            feature = "deploy",
39            feature = "deploy_integration",
40            feature = "embedded_runtime"
41        )))
42    )]
43    Legacy {
44        /// The raw numeric identifier for this cluster member.
45        raw_id: u32,
46    },
47    /// A Docker container-based member ID, used with the `docker_runtime` feature.
48    #[cfg(feature = "docker_runtime")]
49    #[cfg_attr(docsrs, doc(cfg(feature = "docker_runtime")))]
50    Docker {
51        /// The Docker container name identifying this cluster member.
52        container_name: String,
53    },
54    /// A Maelstrom node-based member ID, used with the `maelstrom_runtime` feature.
55    #[cfg(feature = "maelstrom_runtime")]
56    #[cfg_attr(docsrs, doc(cfg(feature = "maelstrom_runtime")))]
57    Maelstrom {
58        /// The Maelstrom node ID string identifying this cluster member.
59        node_id: String,
60    },
61}
62
63macro_rules! assert_feature {
64    (#[cfg($meta:meta)] $( $code:stmt )+) => {
65        #[cfg(not($meta))]
66        panic!("Feature {:?} is not enabled.", stringify!($meta));
67
68        #[cfg($meta)]
69        {
70            $( $code )+
71        }
72    };
73}
74
75impl TaglessMemberId {
76    /// Creates a [`TaglessMemberId`] from a raw numeric ID.
77    ///
78    /// # Panics
79    /// Panics if the `deploy` / `deploy_integration` / `embedded_runtime` feature is not enabled.
80    pub fn from_raw_id(_raw_id: u32) -> Self {
81        assert_feature! {
82            #[cfg(any(feature = "deploy", feature = "deploy_integration", feature = "embedded_runtime"))]
83            Self::Legacy { raw_id: _raw_id }
84        }
85    }
86
87    /// Returns the raw numeric ID from this member identifier.
88    ///
89    /// # Panics
90    /// Panics if this is not the `Legacy` variant or if the `deploy_integration`
91    /// feature is not enabled.
92    pub fn get_raw_id(&self) -> u32 {
93        assert_feature! {
94            #[cfg(feature = "deploy_integration")]
95            #[expect(clippy::allow_attributes, reason = "Depends on features.")]
96            #[allow(
97                irrefutable_let_patterns,
98                reason = "Depends on features."
99            )]
100            let TaglessMemberId::Legacy { raw_id } = self else {
101                panic!("Not `Legacy` variant.");
102            }
103            *raw_id
104        }
105    }
106
107    /// Creates a [`TaglessMemberId`] from a Docker container name.
108    ///
109    /// # Panics
110    /// Panics if the `docker_runtime` feature is not enabled.
111    pub fn from_container_name(_container_name: impl Into<String>) -> Self {
112        assert_feature! {
113            #[cfg(feature = "docker_runtime")]
114            Self::Docker {
115                container_name: _container_name.into(),
116            }
117        }
118    }
119
120    /// Returns the Docker container name from this member identifier.
121    ///
122    /// # Panics
123    /// Panics if this is not the `Docker` variant or if the `docker_runtime`
124    /// feature is not enabled.
125    pub fn get_container_name(&self) -> &str {
126        assert_feature! {
127            #[cfg(feature = "docker_runtime")]
128            #[expect(clippy::allow_attributes, reason = "Depends on features.")]
129            #[allow(
130                irrefutable_let_patterns,
131                reason = "Depends on features."
132            )]
133            let TaglessMemberId::Docker { container_name } = self else {
134                panic!("Not `Docker` variant.");
135            }
136            container_name
137        }
138    }
139
140    /// Creates a [`TaglessMemberId`] from a Maelstrom node ID.
141    ///
142    /// # Panics
143    /// Panics if the `maelstrom_runtime` feature is not enabled.
144    pub fn from_maelstrom_node_id(_node_id: impl Into<String>) -> Self {
145        assert_feature! {
146                #[cfg(feature = "maelstrom_runtime")]
147                Self::Maelstrom {
148                node_id: _node_id.into(),
149            }
150        }
151    }
152
153    /// Returns the Maelstrom node ID from this member identifier.
154    ///
155    /// # Panics
156    /// Panics if this is not the `Maelstrom` variant or if the `maelstrom_runtime`
157    /// feature is not enabled.
158    pub fn get_maelstrom_node_id(&self) -> &str {
159        assert_feature! {
160            #[cfg(feature = "maelstrom_runtime")]
161            #[expect(clippy::allow_attributes, reason = "Depends on features.")]
162            #[allow(
163                irrefutable_let_patterns,
164                reason = "Depends on features."
165            )]
166            let TaglessMemberId::Maelstrom { node_id } = self else {
167                panic!("Not `Maelstrom` variant.");
168            }
169            node_id
170        }
171    }
172}
173
174impl Display for TaglessMemberId {
175    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            #[cfg(feature = "deploy_integration")]
178            TaglessMemberId::Legacy { raw_id } => write!(_f, "{}", raw_id),
179            #[cfg(feature = "docker_runtime")]
180            TaglessMemberId::Docker { container_name } => write!(_f, "{}", container_name),
181            #[cfg(feature = "maelstrom_runtime")]
182            TaglessMemberId::Maelstrom { node_id } => write!(_f, "{}", node_id),
183            #[expect(
184                clippy::allow_attributes,
185                reason = "Only triggers when `TaglessMemberId` is empty."
186            )]
187            #[allow(
188                unreachable_patterns,
189                reason = "Needed when `TaglessMemberId` is empty."
190            )]
191            _ => panic!(),
192        }
193    }
194}
195
196/// A typed identifier for a member of a [`Cluster`](super::Cluster).
197///
198/// The `Tag` type parameter ties this ID to a specific cluster, preventing
199/// accidental mixing of member IDs from different clusters at compile time.
200/// Under the hood, this wraps a [`TaglessMemberId`].
201#[repr(transparent)]
202pub struct MemberId<Tag> {
203    inner: TaglessMemberId,
204    _phantom: PhantomData<Tag>,
205}
206
207impl<Tag> MemberId<Tag> {
208    /// Converts this typed member ID into an untyped [`TaglessMemberId`],
209    /// discarding the compile-time cluster tag.
210    pub fn into_tagless(self) -> TaglessMemberId {
211        self.inner
212    }
213
214    /// Creates a typed [`MemberId`] from an untyped [`TaglessMemberId`].
215    pub fn from_tagless(inner: TaglessMemberId) -> Self {
216        Self {
217            inner,
218            _phantom: Default::default(),
219        }
220    }
221
222    /// Creates a typed [`MemberId`] from a raw numeric ID.
223    ///
224    /// # Panics
225    /// Panics if the `deploy_integration` feature is not enabled.
226    pub fn from_raw_id(raw_id: u32) -> Self {
227        #[expect(clippy::allow_attributes, reason = "Depends on features.")]
228        #[allow(
229            unreachable_code,
230            reason = "`inner` may be uninhabited depending on features."
231        )]
232        Self {
233            inner: TaglessMemberId::from_raw_id(raw_id),
234            _phantom: Default::default(),
235        }
236    }
237
238    /// Returns the raw numeric ID from this member identifier.
239    ///
240    /// # Panics
241    /// Panics if the underlying [`TaglessMemberId`] is not the `Legacy` variant
242    /// or if the `deploy_integration` feature is not enabled.
243    pub fn get_raw_id(&self) -> u32 {
244        self.inner.get_raw_id()
245    }
246}
247
248impl<Tag> Debug for MemberId<Tag> {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        Display::fmt(self, f)
251    }
252}
253
254impl<Tag> Display for MemberId<Tag> {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        write!(
257            f,
258            "MemberId::<{}>({})",
259            std::any::type_name::<Tag>(),
260            self.inner
261        )
262    }
263}
264
265impl<Tag> Clone for MemberId<Tag> {
266    fn clone(&self) -> Self {
267        #[expect(clippy::allow_attributes, reason = "Depends on features.")]
268        #[allow(
269            unreachable_code,
270            reason = "`inner` may be uninhabited depending on features."
271        )]
272        Self {
273            inner: self.inner.clone(),
274            _phantom: Default::default(),
275        }
276    }
277}
278
279impl<Tag> Serialize for MemberId<Tag> {
280    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
281    where
282        S: serde::Serializer,
283    {
284        self.inner.serialize(serializer)
285    }
286}
287
288impl<'a, Tag> Deserialize<'a> for MemberId<Tag> {
289    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290    where
291        D: serde::Deserializer<'a>,
292    {
293        #[expect(clippy::allow_attributes, reason = "Depends on features.")]
294        #[allow(
295            unreachable_code,
296            reason = "`inner` may be uninhabited depending on features."
297        )]
298        Ok(Self::from_tagless(TaglessMemberId::deserialize(
299            deserializer,
300        )?))
301    }
302}
303
304impl<Tag> PartialOrd for MemberId<Tag> {
305    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
306        Some(self.cmp(other))
307    }
308}
309
310impl<Tag> Ord for MemberId<Tag> {
311    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
312        self.inner.cmp(&other.inner)
313    }
314}
315
316impl<Tag> PartialEq for MemberId<Tag> {
317    fn eq(&self, other: &Self) -> bool {
318        self.inner == other.inner
319    }
320}
321
322impl<Tag> Eq for MemberId<Tag> {}
323
324impl<Tag> Hash for MemberId<Tag> {
325    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
326        self.inner.hash(state);
327        // This seems like the a good thing to do. This will ensure that two member ids that come from different
328        // clusters but the same underlying host receive different hashes.
329        std::any::type_name::<Tag>().hash(state);
330    }
331}