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