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