hyperactor_mesh/
v1.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! A temporary holding space for APIv1 of the Hyperactor Mesh.
10//! This will be moved down to the base module when we graduate
11//! the APIs and fully deprecate the "v0" APIs.
12
13pub mod actor_mesh;
14pub mod host_mesh;
15pub mod proc_mesh;
16pub mod testactor;
17pub mod testing;
18pub mod value_mesh;
19
20use std::str::FromStr;
21
22pub use actor_mesh::ActorMesh;
23pub use actor_mesh::ActorMeshRef;
24pub use host_mesh::HostMeshRef;
25use hyperactor::ActorId;
26use hyperactor::ActorRef;
27use hyperactor::mailbox::MailboxSenderError;
28use ndslice::view;
29pub use proc_mesh::ProcMesh;
30pub use proc_mesh::ProcMeshRef;
31use serde::Deserialize;
32use serde::Serialize;
33pub use value_mesh::ValueMesh;
34
35use crate::resource;
36use crate::shortuuid::ShortUuid;
37use crate::v1::host_mesh::HostMeshAgent;
38use crate::v1::host_mesh::HostMeshRefParseError;
39
40/// Errors that occur during mesh operations.
41#[derive(Debug, thiserror::Error)]
42pub enum Error {
43    #[error("invalid mesh ref: expected {expected} ranks, but contains {actual} ranks")]
44    InvalidRankCardinality { expected: usize, actual: usize },
45
46    #[error(transparent)]
47    NameParseError(#[from] NameParseError),
48
49    #[error(transparent)]
50    HostMeshRefParseError(#[from] HostMeshRefParseError),
51
52    #[error(transparent)]
53    AllocatorError(#[from] Box<crate::alloc::AllocatorError>),
54
55    #[error(transparent)]
56    ChannelError(#[from] Box<hyperactor::channel::ChannelError>),
57
58    #[error(transparent)]
59    MailboxError(#[from] Box<hyperactor::mailbox::MailboxError>),
60
61    #[error(transparent)]
62    CodecError(#[from] CodecError),
63
64    #[error("error during mesh configuration: {0}")]
65    ConfigurationError(anyhow::Error),
66
67    // This is a temporary error to ensure we don't create unroutable
68    // meshes.
69    #[error("configuration error: mesh is unroutable")]
70    UnroutableMesh(),
71
72    #[error("error while calling actor {0}: {1}")]
73    CallError(ActorId, anyhow::Error),
74
75    #[error("actor not registered for type {0}")]
76    ActorTypeNotRegistered(String),
77
78    // TODO: this should be a valuemesh of statuses
79    #[error("error while spawning actor {0}: {1}")]
80    GspawnError(Name, String),
81
82    #[error("error while sending message to actor {0}: {1}")]
83    SendingError(ActorId, Box<MailboxSenderError>),
84
85    #[error("error while casting message to {0}: {1}")]
86    CastingError(Name, anyhow::Error),
87
88    #[error("error configuring host mesh agent {0}: {1}")]
89    HostMeshAgentConfigurationError(ActorId, String),
90
91    #[error("error creating {proc_name} (host rank {host_rank}) on host mesh agent {mesh_agent}")]
92    ProcCreationError {
93        proc_name: Name,
94        mesh_agent: ActorRef<HostMeshAgent>,
95        host_rank: usize,
96        status: resource::Status,
97    },
98
99    #[error("error: {0} does not exist")]
100    NotExist(Name),
101}
102
103/// Errors that occur during serialization and deserialization.
104#[derive(Debug, thiserror::Error)]
105pub enum CodecError {
106    #[error(transparent)]
107    BincodeError(#[from] Box<bincode::Error>),
108    #[error(transparent)]
109    JsonError(#[from] Box<serde_json::Error>),
110    #[error(transparent)]
111    Base64Error(#[from] Box<base64::DecodeError>),
112    #[error(transparent)]
113    Utf8Error(#[from] Box<std::str::Utf8Error>),
114}
115
116impl From<bincode::Error> for Error {
117    fn from(e: bincode::Error) -> Self {
118        Error::CodecError(Box::new(e).into())
119    }
120}
121
122impl From<serde_json::Error> for Error {
123    fn from(e: serde_json::Error) -> Self {
124        Error::CodecError(Box::new(e).into())
125    }
126}
127
128impl From<base64::DecodeError> for Error {
129    fn from(e: base64::DecodeError) -> Self {
130        Error::CodecError(Box::new(e).into())
131    }
132}
133
134impl From<std::str::Utf8Error> for Error {
135    fn from(e: std::str::Utf8Error) -> Self {
136        Error::CodecError(Box::new(e).into())
137    }
138}
139
140impl From<crate::alloc::AllocatorError> for Error {
141    fn from(e: crate::alloc::AllocatorError) -> Self {
142        Error::AllocatorError(Box::new(e))
143    }
144}
145
146impl From<hyperactor::channel::ChannelError> for Error {
147    fn from(e: hyperactor::channel::ChannelError) -> Self {
148        Error::ChannelError(Box::new(e))
149    }
150}
151
152impl From<hyperactor::mailbox::MailboxError> for Error {
153    fn from(e: hyperactor::mailbox::MailboxError) -> Self {
154        Error::MailboxError(Box::new(e))
155    }
156}
157
158impl From<view::InvalidCardinality> for crate::v1::Error {
159    fn from(e: view::InvalidCardinality) -> Self {
160        crate::v1::Error::InvalidRankCardinality {
161            expected: e.expected,
162            actual: e.actual,
163        }
164    }
165}
166
167/// The type of result used in `hyperactor_mesh::v1`.
168pub type Result<T> = std::result::Result<T, Error>;
169
170/// Names are used to identify objects in the system. They have a user-provided name,
171/// and a unique UUID.
172///
173/// Names have a concrete syntax--`{name}-{uuid}`--printed by `Display` and parsed by `FromStr`.
174#[derive(
175    Debug,
176    Clone,
177    PartialEq,
178    Eq,
179    PartialOrd,
180    Ord,
181    Hash,
182    Serialize,
183    Deserialize
184)]
185pub enum Name {
186    /// Normal names for most actors.
187    Suffixed(String, ShortUuid),
188    /// Reserved names for system actors without UUIDs.
189    Reserved(String),
190}
191
192// The delimiter between the name and the uuid when a Name::Suffixed is stringified.
193// Actor names must be parseable as a Rust identifier, so this delimiter must be
194// something that is part of a valid Rust identifier.
195static NAME_SUFFIX_DELIMITER: &str = "_";
196
197impl Name {
198    /// Create a new `Name` from a user-provided base name.
199    pub fn new(name: impl Into<String>) -> Self {
200        Self::new_with_uuid(name, Some(ShortUuid::generate()))
201    }
202
203    /// Create a Reserved `Name` with no uuid. Only for use by system actors.
204    pub(crate) fn new_reserved(name: impl Into<String>) -> Self {
205        Self::new_with_uuid(name, None)
206    }
207
208    fn new_with_uuid(name: impl Into<String>, uuid: Option<ShortUuid>) -> Self {
209        let mut name = name.into();
210        if name.is_empty() {
211            name = "unnamed".to_string();
212        }
213        if let Some(uuid) = uuid {
214            Self::Suffixed(name, uuid)
215        } else {
216            Self::Reserved(name)
217        }
218    }
219
220    /// The name portion of this `Name`.
221    pub fn name(&self) -> &str {
222        match self {
223            Self::Suffixed(n, _) => n,
224            Self::Reserved(n) => n,
225        }
226    }
227
228    /// The UUID portion of this `Name`.
229    /// Only Some for Name::Suffixed, if called on Name::Reserved it'll be None.
230    pub fn uuid(&self) -> Option<&ShortUuid> {
231        match self {
232            Self::Suffixed(_, uuid) => Some(uuid),
233            Self::Reserved(_) => None,
234        }
235    }
236}
237
238/// Errors that occur when parsing names.
239#[derive(thiserror::Error, Debug)]
240pub enum NameParseError {
241    #[error("invalid name: missing name")]
242    MissingName,
243
244    #[error("invalid name: missing uuid")]
245    MissingUuid,
246
247    #[error(transparent)]
248    InvalidUuid(#[from] <ShortUuid as FromStr>::Err),
249
250    #[error("invalid name: missing separator")]
251    MissingSeparator,
252}
253
254impl FromStr for Name {
255    type Err = NameParseError;
256
257    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
258        if let Some((name, uuid)) = s.split_once(NAME_SUFFIX_DELIMITER) {
259            if name.is_empty() {
260                return Err(NameParseError::MissingName);
261            }
262            if uuid.is_empty() {
263                return Err(NameParseError::MissingName);
264            }
265
266            Ok(Name::new_with_uuid(name.to_string(), Some(uuid.parse()?)))
267        } else {
268            if s.is_empty() {
269                return Err(NameParseError::MissingName);
270            }
271            Ok(Name::new_reserved(s))
272        }
273    }
274}
275
276impl std::fmt::Display for Name {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        match self {
279            Self::Suffixed(n, uuid) => {
280                write!(f, "{}{}", n, NAME_SUFFIX_DELIMITER)?;
281                uuid.format(f, true /*raw*/)
282            }
283            Self::Reserved(n) => write!(f, "{}", n),
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_name_unique() {
294        assert_ne!(Name::new("foo"), Name::new("foo"));
295        let name = Name::new("foo");
296        assert_eq!(name, name);
297    }
298
299    #[test]
300    fn test_name_roundtrip() {
301        let uuid = "111111111111".parse::<ShortUuid>().unwrap();
302        let name = Name::new_with_uuid("foo", Some(uuid));
303        let str = name.to_string();
304        assert_eq!(str, "foo_111111111111");
305        assert_eq!(name, Name::from_str(&str).unwrap());
306    }
307
308    #[test]
309    fn test_name_roundtrip_with_underscore() {
310        // A ShortUuid may have an underscore prefix if the first character is a digit.
311        // Make sure this doesn't impact parsing.
312        let uuid = "_1a2b3c4d5e6f".parse::<ShortUuid>().unwrap();
313        let name = Name::new_with_uuid("foo", Some(uuid));
314        let str = name.to_string();
315        // Leading underscore is stripped as not needed.
316        assert_eq!(str, "foo_1a2b3c4d5e6f");
317        assert_eq!(name, Name::from_str(&str).unwrap());
318    }
319
320    #[test]
321    fn test_name_roundtrip_random() {
322        let name = Name::new("foo");
323        assert_eq!(name, Name::from_str(&name.to_string()).unwrap());
324    }
325
326    #[test]
327    fn test_name_roundtrip_reserved() {
328        let name = Name::new_reserved("foo");
329        let str = name.to_string();
330        assert_eq!(str, "foo");
331        assert_eq!(name, Name::from_str(&str).unwrap());
332    }
333
334    #[test]
335    fn test_name_parse() {
336        // Multiple underscores are allowed in the name, as ShortUuid will discard
337        // them.
338        let name = Name::from_str("foo__1a2b3c4_d5e6f").unwrap();
339        assert_eq!(format!("{}", name), "foo_1a2b3c4d5e6f");
340    }
341}