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