1use std::cmp::Ordering;
51use std::fmt;
52use std::hash::Hash;
53use std::hash::Hasher;
54use std::str::FromStr;
55
56use hyperactor::ActorAddr;
57use hyperactor::ActorId;
58use hyperactor::Location;
59use hyperactor::ProcAddr;
60use hyperactor::ProcId;
61use hyperactor::id::Label;
62use hyperactor::id::LabelError;
63use hyperactor::id::Uid;
64use hyperactor::id::UidParseError;
65use serde::Deserialize;
66use serde::Serialize;
67use typeuri::Named;
68
69const RESOURCE_ID_DEFAULT_LABEL: &str = "resource";
70const HOST_MESH_ID_DEFAULT_LABEL: &str = "host";
71const PROC_MESH_ID_DEFAULT_LABEL: &str = "proc";
72const ACTOR_MESH_ID_DEFAULT_LABEL: &str = "actor";
73
74#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
76pub enum ResourceIdParseError {
77 #[error("invalid uid: {0}")]
79 InvalidUid(#[from] UidParseError),
80 #[error("invalid label: {0}")]
82 InvalidLabel(#[from] LabelError),
83}
84
85#[derive(Clone, Serialize, Deserialize, Named)]
89pub struct ResourceId(Uid);
90wirevalue::register_type!(ResourceId);
91
92impl ResourceId {
93 pub fn new(uid: Uid, label: Option<Label>) -> Self {
95 Self(uid.with_label(label))
96 }
97
98 pub fn singleton(label: Label) -> Self {
101 Self(Uid::Singleton(label))
102 }
103
104 pub fn instance(label: Label) -> Self {
106 Self(Uid::instance(label))
107 }
108
109 pub fn from_name(name: impl AsRef<str>) -> Self {
114 name.as_ref()
115 .parse()
116 .unwrap_or_else(|_| Self::singleton(Label::strip(name.as_ref())))
117 }
118
119 pub fn uid(&self) -> &Uid {
121 &self.0
122 }
123
124 pub fn label(&self) -> Option<&Label> {
126 match &self.0 {
127 Uid::Singleton(_) => None,
128 Uid::Instance(_, label) => label.as_ref(),
129 }
130 }
131
132 pub fn display_label(&self) -> Option<&Label> {
137 self.0.label()
138 }
139
140 pub fn proc_id(&self) -> ProcId {
142 ProcId::new(self.0.clone(), None)
143 }
144
145 pub fn proc_addr(&self, location: impl Into<Location>) -> ProcAddr {
147 ProcAddr::new(self.proc_id(), location.into())
148 }
149
150 pub fn proc_addr_from_name(location: impl Into<Location>, name: impl AsRef<str>) -> ProcAddr {
152 Self::from_name(name).proc_addr(location)
153 }
154}
155
156impl From<ResourceId> for Uid {
157 fn from(id: ResourceId) -> Self {
158 id.0
159 }
160}
161
162impl From<&ResourceId> for Uid {
163 fn from(id: &ResourceId) -> Self {
164 id.0.clone()
165 }
166}
167
168impl From<ResourceId> for ProcId {
169 fn from(id: ResourceId) -> Self {
170 Self::new(id.0, None)
171 }
172}
173
174impl From<&ResourceId> for ProcId {
175 fn from(id: &ResourceId) -> Self {
176 id.proc_id()
177 }
178}
179
180impl From<ProcId> for ResourceId {
181 fn from(id: ProcId) -> Self {
182 Self(id.uid().clone())
183 }
184}
185
186impl From<&ProcId> for ResourceId {
187 fn from(id: &ProcId) -> Self {
188 Self(id.uid().clone())
189 }
190}
191
192impl From<&ProcAddr> for ResourceId {
193 fn from(addr: &ProcAddr) -> Self {
194 Self::from(addr.id())
195 }
196}
197
198impl PartialEq for ResourceId {
199 fn eq(&self, other: &Self) -> bool {
200 self.0 == other.0
201 }
202}
203
204impl Eq for ResourceId {}
205
206impl Hash for ResourceId {
207 fn hash<H: Hasher>(&self, state: &mut H) {
208 self.0.hash(state);
209 }
210}
211
212impl PartialOrd for ResourceId {
213 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
214 Some(self.cmp(other))
215 }
216}
217
218impl Ord for ResourceId {
219 fn cmp(&self, other: &Self) -> Ordering {
220 self.0.cmp(&other.0)
221 }
222}
223
224fn fmt_instance_uid(uid: u64) -> String {
225 Uid::Instance(uid, None)
226 .instance_uid_base58()
227 .expect("instance uid should have base58 representation")
228}
229
230fn parse_instance_uid(s: &str) -> Result<u64, UidParseError> {
231 Uid::parse_instance_uid_base58(s)
232}
233
234fn fmt_id_component(
235 f: &mut fmt::Formatter<'_>,
236 uid: &Uid,
237 label: Option<&Label>,
238 default_instance_label: &str,
239) -> fmt::Result {
240 match uid {
241 Uid::Singleton(singleton) => write!(f, "{singleton}"),
242 Uid::Instance(uid, _) => match label {
243 Some(label) => write!(f, "{label}-{}", fmt_instance_uid(*uid)),
244 None => write!(f, "{}-{}", default_instance_label, fmt_instance_uid(*uid)),
245 },
246 }
247}
248
249impl fmt::Display for ResourceId {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 fmt_id_component(f, &self.0, self.label(), RESOURCE_ID_DEFAULT_LABEL)
256 }
257}
258
259impl fmt::Debug for ResourceId {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match (&self.0, self.label()) {
262 (Uid::Singleton(label), _) => write!(f, "<{label}>"),
263 (Uid::Instance(uid, _), Some(label)) => {
264 write!(f, "<'{label}' {}>", fmt_instance_uid(*uid))
265 }
266 (Uid::Instance(uid, _), None) => write!(f, "<{}>", fmt_instance_uid(*uid)),
267 }
268 }
269}
270
271fn parse_id_component(s: &str, default_instance_label: &str) -> Result<Uid, ResourceIdParseError> {
272 if let Some(split) = s.rfind('-') {
273 let label_part = &s[..split];
274 let uid_part = &s[split + 1..];
275 if uid_part.len() >= 8
276 && let (Ok(label), Ok(uid)) = (Label::new(label_part), parse_instance_uid(uid_part))
277 {
278 if label.as_str() == default_instance_label {
279 return Ok(Uid::Instance(uid, None));
280 }
281 return Ok(Uid::Instance(uid, Some(label)));
282 }
283 }
284
285 let label = Label::new(s)?;
286 Ok(Uid::Singleton(label))
287}
288
289impl FromStr for ResourceId {
290 type Err = ResourceIdParseError;
291
292 fn from_str(s: &str) -> Result<Self, Self::Err> {
301 Ok(Self(parse_id_component(s, RESOURCE_ID_DEFAULT_LABEL)?))
302 }
303}
304
305macro_rules! define_mesh_id {
306 ($(#[$meta:meta])* $name:ident, $default_label:expr) => {
307 $(#[$meta])*
308 #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Named)]
309 #[serde(transparent)]
310 pub struct $name(ResourceId);
311 wirevalue::register_type!($name);
312
313 impl $name {
314 pub fn new(uid: Uid, label: Option<Label>) -> Self {
316 Self(ResourceId::new(uid, label))
317 }
318
319 pub fn singleton(label: Label) -> Self {
321 Self(ResourceId::singleton(label))
322 }
323
324 pub fn instance(label: Label) -> Self {
326 Self(ResourceId::instance(label))
327 }
328
329 pub fn uid(&self) -> &Uid {
331 self.0.uid()
332 }
333
334 pub fn label(&self) -> Option<&Label> {
336 self.0.label()
337 }
338
339 pub fn display_label(&self) -> Option<&Label> {
343 self.0.display_label()
344 }
345
346 pub fn resource_id(&self) -> &ResourceId {
348 &self.0
349 }
350
351 pub fn default_instance_label() -> &'static str {
353 $default_label
354 }
355 }
356
357 impl From<$name> for ResourceId {
358 fn from(id: $name) -> Self {
359 id.0
360 }
361 }
362
363 impl From<ResourceId> for $name {
364 fn from(id: ResourceId) -> Self {
365 Self(id)
366 }
367 }
368
369
370 impl fmt::Display for $name {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 fmt_id_component(f, self.uid(), self.label(), Self::default_instance_label())
373 }
374 }
375
376 impl fmt::Debug for $name {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 fmt::Debug::fmt(&self.0, f)
379 }
380 }
381
382 impl FromStr for $name {
383 type Err = ResourceIdParseError;
384
385 fn from_str(s: &str) -> Result<Self, Self::Err> {
386 Ok(Self(ResourceId(parse_id_component(
387 s,
388 Self::default_instance_label(),
389 )?)))
390 }
391 }
392 };
393}
394
395define_mesh_id!(
396 HostMeshId,
398 HOST_MESH_ID_DEFAULT_LABEL
399);
400
401define_mesh_id!(
402 ProcMeshId,
404 PROC_MESH_ID_DEFAULT_LABEL
405);
406
407define_mesh_id!(
408 ActorMeshId,
410 ACTOR_MESH_ID_DEFAULT_LABEL
411);
412
413impl ProcMeshId {
414 pub fn proc_id(&self) -> ProcId {
416 self.resource_id().proc_id()
417 }
418
419 pub fn proc_addr(&self, location: impl Into<Location>) -> ProcAddr {
421 self.resource_id().proc_addr(location)
422 }
423}
424
425impl ActorMeshId {
426 pub fn actor_id(&self, proc_id: ProcId) -> ActorId {
428 ActorId::new(self.uid().clone(), proc_id, None)
429 }
430
431 pub fn actor_addr(&self, proc_addr: ProcAddr) -> ActorAddr {
433 ActorAddr::new_from_uid(proc_addr, self.uid().clone())
434 }
435}
436
437impl hyperactor_config::AttrValue for ActorMeshId {
438 fn display(&self) -> String {
439 self.to_string()
440 }
441
442 fn parse(value: &str) -> Result<Self, anyhow::Error> {
443 Ok(value.parse()?)
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use std::collections::hash_map::DefaultHasher;
450
451 use super::*;
452
453 #[test]
454 fn test_resource_id_singleton() {
455 let id = ResourceId::singleton(Label::new("local").unwrap());
456 assert_eq!(*id.uid(), Uid::Singleton(Label::new("local").unwrap()));
457 assert_eq!(id.label(), None);
458 assert_eq!(id.to_string(), "local");
459 }
460
461 #[test]
462 fn test_resource_id_instance() {
463 let id = ResourceId::instance(Label::new("workers").unwrap());
464 assert!(id.uid().is_instance());
465 assert_eq!(id.label().map(|l| l.as_str()), Some("workers"));
466 }
467
468 #[test]
469 fn test_resource_id_unlabeled() {
470 let id = ResourceId::new(Uid::Instance(0xabcdef, None), None);
471 assert_eq!(
472 id.to_string(),
473 format!("resource-{}", fmt_instance_uid(0xabcdef))
474 );
475 assert_eq!(id.label(), None);
476 }
477
478 #[test]
479 fn test_resource_id_eq_by_uid_only() {
480 let uid = Uid::Instance(0x42, None);
481 let a = ResourceId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
482 let b = ResourceId::new(uid, Some(Label::new("beta").unwrap()));
483 assert_eq!(a, b);
484 }
485
486 #[test]
487 fn test_resource_id_neq_different_uid() {
488 let a = ResourceId::new(Uid::Instance(1, None), Some(Label::new("same").unwrap()));
489 let b = ResourceId::new(Uid::Instance(2, None), Some(Label::new("same").unwrap()));
490 assert_ne!(a, b);
491 }
492
493 #[test]
494 fn test_resource_id_hash_by_uid_only() {
495 let uid = Uid::Instance(0x42, None);
496 let a = ResourceId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
497 let b = ResourceId::new(uid, Some(Label::new("beta").unwrap()));
498
499 let hash = |id: &ResourceId| {
500 let mut h = DefaultHasher::new();
501 id.hash(&mut h);
502 h.finish()
503 };
504 assert_eq!(hash(&a), hash(&b));
505 }
506
507 #[test]
508 fn test_resource_id_ord_by_uid_only() {
509 let a = ResourceId::new(Uid::Instance(1, None), Some(Label::new("zzz").unwrap()));
510 let b = ResourceId::new(Uid::Instance(2, None), Some(Label::new("aaa").unwrap()));
511 assert!(a < b);
512 }
513
514 #[test]
515 fn test_resource_id_display_singleton() {
516 let id = ResourceId::singleton(Label::new("local").unwrap());
517 assert_eq!(id.to_string(), "local");
518 }
519
520 #[test]
521 fn test_resource_id_display_labeled_instance() {
522 let id = ResourceId::new(
523 Uid::Instance(0xd5d54d7201103869, None),
524 Some(Label::new("workers").unwrap()),
525 );
526 assert_eq!(
527 id.to_string(),
528 format!("workers-{}", fmt_instance_uid(0xd5d54d7201103869))
529 );
530 }
531
532 #[test]
533 fn test_resource_id_display_unlabeled_instance() {
534 let id = ResourceId::new(Uid::Instance(0xd5d54d7201103869, None), None);
535 assert_eq!(
536 id.to_string(),
537 format!("resource-{}", fmt_instance_uid(0xd5d54d7201103869))
538 );
539 }
540
541 #[test]
542 fn test_resource_id_debug() {
543 let singleton = ResourceId::singleton(Label::new("local").unwrap());
544 assert_eq!(format!("{:?}", singleton), "<local>");
545
546 let labeled = ResourceId::new(
547 Uid::Instance(0xd5d54d7201103869, None),
548 Some(Label::new("workers").unwrap()),
549 );
550 assert_eq!(
551 format!("{:?}", labeled),
552 format!("<'workers' {}>", fmt_instance_uid(0xd5d54d7201103869))
553 );
554
555 let unlabeled = ResourceId::new(Uid::Instance(0xd5d54d7201103869, None), None);
556 assert_eq!(
557 format!("{:?}", unlabeled),
558 format!("<{}>", fmt_instance_uid(0xd5d54d7201103869))
559 );
560 }
561
562 #[test]
563 fn test_resource_id_fromstr_singleton() {
564 let parsed: ResourceId = "local".parse().unwrap();
565 assert_eq!(*parsed.uid(), Uid::Singleton(Label::new("local").unwrap()));
566 assert_eq!(parsed.label(), None);
567 }
568
569 #[test]
570 fn test_resource_id_fromstr_base58_like_singleton() {
571 let parsed: ResourceId = "service".parse().unwrap();
572 assert_eq!(
573 *parsed.uid(),
574 Uid::Singleton(Label::new("service").unwrap())
575 );
576 assert_eq!(parsed.label(), None);
577 }
578
579 #[test]
580 fn test_resource_id_fromstr_short_suffix_singleton() {
581 let parsed: ResourceId = "env-vars".parse().unwrap();
582 assert_eq!(
583 *parsed.uid(),
584 Uid::Singleton(Label::new("env-vars").unwrap())
585 );
586 assert_eq!(parsed.label(), None);
587 }
588
589 #[test]
590 fn test_resource_id_fromstr_labeled_instance() {
591 let parsed: ResourceId = format!("workers-{}", fmt_instance_uid(0xd5d54d7201103869))
592 .parse()
593 .unwrap();
594 assert_eq!(
595 *parsed.uid(),
596 Uid::Instance(0xd5d54d7201103869, Some(Label::new("workers").unwrap()))
597 );
598 assert_eq!(parsed.label().map(|l| l.as_str()), Some("workers"));
599 }
600
601 #[test]
602 fn test_resource_id_fromstr_default_labeled_instance() {
603 let parsed: ResourceId = format!("resource-{}", fmt_instance_uid(0xd5d54d7201103869))
604 .parse()
605 .unwrap();
606 assert_eq!(*parsed.uid(), Uid::Instance(0xd5d54d7201103869, None));
607 assert_eq!(parsed.label(), None);
608 }
609
610 #[test]
611 fn test_resource_id_fromstr_rejects_unlabeled_instance() {
612 let result: Result<ResourceId, _> =
613 format!("<{}>", fmt_instance_uid(0xd5d54d7201103869)).parse();
614 assert!(result.is_err());
615 }
616
617 #[test]
618 fn test_resource_id_fromstr_labeled_with_hyphens() {
619 let parsed: ResourceId = format!("my-service-{}", fmt_instance_uid(0xd5d54d7201103869))
620 .parse()
621 .unwrap();
622 assert_eq!(
623 *parsed.uid(),
624 Uid::Instance(0xd5d54d7201103869, Some(Label::new("my-service").unwrap()))
625 );
626 assert_eq!(parsed.label().map(|l| l.as_str()), Some("my-service"));
627 }
628
629 #[test]
630 fn test_resource_id_display_fromstr_roundtrip() {
631 let cases = vec![
632 ResourceId::singleton(Label::new("local").unwrap()),
633 ResourceId::new(
634 Uid::Instance(0xd5d54d7201103869, None),
635 Some(Label::new("workers").unwrap()),
636 ),
637 ResourceId::new(Uid::Instance(0xd5d54d7201103869, None), None),
638 ResourceId::new(
639 Uid::Instance(0xd5d54d7201103869, None),
640 Some(Label::new("my-service").unwrap()),
641 ),
642 ResourceId::new(
643 Uid::Instance(0xd5d54d7201103869, None),
644 Some(Label::new("a").unwrap()),
645 ),
646 ];
647 for id in cases {
648 let s = id.to_string();
649 let parsed: ResourceId = s.parse().unwrap();
650 assert_eq!(id, parsed, "round-trip failed for {s}");
651 }
652 }
653
654 #[test]
655 fn test_resource_id_serde_roundtrip() {
656 let cases = vec![
657 ResourceId::singleton(Label::new("local").unwrap()),
658 ResourceId::new(
659 Uid::Instance(0xabcdef, None),
660 Some(Label::new("workers").unwrap()),
661 ),
662 ResourceId::new(Uid::Instance(0xabcdef, None), None),
663 ];
664 for id in cases {
665 let json = serde_json::to_string(&id).unwrap();
666 let parsed: ResourceId = serde_json::from_str(&json).unwrap();
667 assert_eq!(id, parsed);
668 assert_eq!(
670 id.label().map(|l| l.as_str()),
671 parsed.label().map(|l| l.as_str())
672 );
673 }
674 }
675
676 #[test]
677 fn test_mesh_id_construction() {
678 let host = HostMeshId::singleton(Label::new("local").unwrap());
679 assert_eq!(host.to_string(), "local");
680 assert_eq!(*host.uid(), Uid::Singleton(Label::new("local").unwrap()));
681
682 let proc_ = ProcMeshId::instance(Label::new("workers").unwrap());
683 assert!(proc_.uid().is_instance());
684 assert_eq!(proc_.label().map(|l| l.as_str()), Some("workers"));
685
686 let actor = ActorMeshId::instance(Label::new("trainers").unwrap());
687 assert!(actor.uid().is_instance());
688 assert_eq!(actor.label().map(|l| l.as_str()), Some("trainers"));
689 }
690
691 #[test]
692 fn test_mesh_id_eq_by_uid_only() {
693 let uid = Uid::Instance(0x42, None);
694 let a = HostMeshId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
695 let b = HostMeshId::new(uid, Some(Label::new("beta").unwrap()));
696 assert_eq!(a, b);
697 }
698
699 #[test]
700 fn test_mesh_id_display_fromstr_roundtrip() {
701 let ids: Vec<HostMeshId> = vec![
702 HostMeshId::singleton(Label::new("local").unwrap()),
703 HostMeshId::new(
704 Uid::Instance(0xd5d54d7201103869, None),
705 Some(Label::new("workers").unwrap()),
706 ),
707 HostMeshId::new(Uid::Instance(0xd5d54d7201103869, None), None),
708 ];
709 for id in ids {
710 let s = id.to_string();
711 let parsed: HostMeshId = s.parse().unwrap();
712 assert_eq!(id, parsed, "round-trip failed for {s}");
713 }
714 }
715
716 #[test]
717 fn test_typed_mesh_id_display_uses_type_default_label() {
718 let uid = Uid::Instance(0xd5d54d7201103869, None);
719 assert_eq!(
720 HostMeshId::new(uid.clone(), None).to_string(),
721 format!("host-{}", fmt_instance_uid(0xd5d54d7201103869))
722 );
723 assert_eq!(
724 ProcMeshId::new(uid.clone(), None).to_string(),
725 format!("proc-{}", fmt_instance_uid(0xd5d54d7201103869))
726 );
727 assert_eq!(
728 ActorMeshId::new(uid, None).to_string(),
729 format!("actor-{}", fmt_instance_uid(0xd5d54d7201103869))
730 );
731 }
732
733 #[test]
734 fn test_typed_mesh_id_parse_omits_type_default_label() {
735 let proc: ProcMeshId = format!("proc-{}", fmt_instance_uid(0xd5d54d7201103869))
736 .parse()
737 .unwrap();
738 assert_eq!(*proc.uid(), Uid::Instance(0xd5d54d7201103869, None));
739 assert_eq!(proc.label(), None);
740
741 let actor: ActorMeshId = format!("actor-{}", fmt_instance_uid(0xd5d54d7201103869))
742 .parse()
743 .unwrap();
744 assert_eq!(*actor.uid(), Uid::Instance(0xd5d54d7201103869, None));
745 assert_eq!(actor.label(), None);
746 }
747
748 #[test]
749 fn test_typed_mesh_id_parse_preserves_non_default_label() {
750 let proc: ProcMeshId = format!("worker-{}", fmt_instance_uid(0xd5d54d7201103869))
751 .parse()
752 .unwrap();
753 assert_eq!(
754 *proc.uid(),
755 Uid::Instance(0xd5d54d7201103869, Some(Label::new("worker").unwrap()))
756 );
757 assert_eq!(proc.label().map(|l| l.as_str()), Some("worker"));
758 }
759
760 #[test]
761 fn test_mesh_id_resource_id_conversion() {
762 let host = HostMeshId::instance(Label::new("test").unwrap());
763 let resource_id: ResourceId = host.clone().into();
764 assert_eq!(host.uid(), resource_id.uid());
765 assert_eq!(
766 host.label().map(|l| l.as_str()),
767 resource_id.label().map(|l| l.as_str())
768 );
769
770 let back: HostMeshId = resource_id.into();
771 assert_eq!(host, back);
772 }
773
774 #[test]
775 fn test_mesh_id_serde_transparent() {
776 let host = HostMeshId::new(
777 Uid::Instance(0xabcdef, None),
778 Some(Label::new("test").unwrap()),
779 );
780 let resource = ResourceId::new(
781 Uid::Instance(0xabcdef, None),
782 Some(Label::new("test").unwrap()),
783 );
784
785 let host_json = serde_json::to_string(&host).unwrap();
786 let resource_json = serde_json::to_string(&resource).unwrap();
787 assert_eq!(host_json, resource_json);
788 }
789
790 #[test]
791 fn test_instance_ids_differ() {
792 let a = ResourceId::instance(Label::new("test").unwrap());
793 let b = ResourceId::instance(Label::new("test").unwrap());
794 assert_ne!(a, b);
795 }
796
797 #[test]
798 fn test_singleton_ids_match() {
799 let a = ResourceId::singleton(Label::new("local").unwrap());
800 let b = ResourceId::singleton(Label::new("local").unwrap());
801 assert_eq!(a, b);
802 }
803}