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