1pub mod mesh;
39
40use core::slice::GetDisjointMutIndex as _;
41use std::collections::HashMap;
42use std::fmt;
43use std::fmt::Debug;
44use std::hash::Hash;
45use std::mem::replace;
46use std::mem::take;
47use std::ops::Deref;
48use std::ops::DerefMut;
49use std::ops::Range;
50use std::time::Duration;
51
52use enum_as_inner::EnumAsInner;
53use hyperactor::HandleClient;
54use hyperactor::Handler;
55use hyperactor::IdleFlushPortRef;
56use hyperactor::PortRef;
57use hyperactor::RefClient;
58use hyperactor::RemoteMessage;
59use hyperactor::mailbox::PortReceiver;
60use hyperactor_config::attrs::Attrs;
61use ndslice::Region;
62use ndslice::ViewExt;
63use serde::Deserialize;
64use serde::Serialize;
65use typeuri::Named;
66
67use crate::StatusOverlay;
68use crate::ValueMesh;
69use crate::bootstrap;
70use crate::bootstrap::BootstrapCommand;
71use crate::bootstrap::ProcBind;
72use crate::host_mesh::host_agent::ProcState;
73use crate::mesh_id::ResourceId;
74use crate::proc_agent::ActorSpec;
75use crate::proc_agent::ActorState;
76
77#[derive(
79 Clone,
80 Debug,
81 Serialize,
82 Deserialize,
83 Named,
84 PartialOrd,
85 Ord,
86 PartialEq,
87 Eq,
88 Hash,
89 EnumAsInner,
90 strum::Display
91)]
92pub enum Status {
93 NotExist,
95 Initializing,
97 Running,
99 Stopping,
101 Stopped,
103 #[strum(to_string = "Failed({0})")]
105 Failed(String),
106 #[strum(to_string = "Timeout({0:?})")]
108 Timeout(Duration),
109 Unknown,
111}
112
113impl Status {
114 pub fn is_terminating(&self) -> bool {
116 matches!(
117 self,
118 Status::Stopping | Status::Stopped | Status::Failed(_) | Status::Timeout(_)
119 )
120 }
121
122 pub fn is_failure(&self) -> bool {
126 matches!(self, Self::Failed(_) | Self::Timeout(_))
127 }
128
129 pub fn is_terminated(&self) -> bool {
132 matches!(
133 self,
134 Status::Stopped | Status::Failed(_) | Status::Timeout(_)
135 )
136 }
137
138 pub fn is_healthy(&self) -> bool {
139 matches!(self, Status::Initializing | Status::Running)
140 }
141
142 pub fn clamp_min(self, floor: Status) -> Status {
149 if floor.is_terminating() && !self.is_terminating() {
150 floor
151 } else {
152 self
153 }
154 }
155}
156
157impl From<bootstrap::ProcStatus> for Status {
158 fn from(status: bootstrap::ProcStatus) -> Self {
159 use bootstrap::ProcStatus;
160 match status {
161 ProcStatus::Starting => Status::Initializing,
162 ProcStatus::Running { .. } | ProcStatus::Ready { .. } => Status::Running,
163 ProcStatus::Stopping { .. } => Status::Stopping,
164 ProcStatus::Stopped { .. } => Status::Stopped,
165 ProcStatus::Failed { reason } => Status::Failed(reason),
166 ProcStatus::Killed { .. } => Status::Failed(format!("{}", status)),
167 }
168 }
169}
170
171impl From<crate::host::LocalProcStatus> for Status {
172 fn from(status: crate::host::LocalProcStatus) -> Self {
173 match status {
174 crate::host::LocalProcStatus::Stopping => Status::Stopping,
175 crate::host::LocalProcStatus::Stopped => Status::Stopped,
176 }
177 }
178}
179
180#[derive(Clone, Debug, Named, PartialEq, Eq, Default)]
184pub struct Rank(pub Option<usize>);
185wirevalue::register_type!(Rank);
186
187#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
189pub struct RankRepr(pub Option<usize>);
190
191impl TryFrom<&Rank> for RankRepr {
192 type Error = serde_multipart::Error;
193
194 fn try_from(rank: &Rank) -> serde_multipart::Result<Self> {
195 Ok(Self(rank.0))
196 }
197}
198
199impl TryFrom<RankRepr> for Rank {
200 type Error = serde_multipart::Error;
201
202 fn try_from(repr: RankRepr) -> serde_multipart::Result<Self> {
203 Ok(Self(repr.0))
204 }
205}
206
207serde_multipart::part_codec! {
208 impl Rank
209 {
210 type Repr = RankRepr;
211 }
212}
213
214impl Rank {
215 pub fn new(rank: usize) -> Self {
217 Self(Some(rank))
218 }
219
220 pub fn unwrap(&self) -> usize {
222 self.0.unwrap()
223 }
224}
225
226#[derive(
233 Clone,
234 Debug,
235 Serialize,
236 Deserialize,
237 Named,
238 Handler,
239 HandleClient,
240 RefClient
241)]
242pub struct GetRankStatus {
243 pub id: ResourceId,
245 pub rank: Rank,
250 pub reply: IdleFlushPortRef<StatusOverlay>,
252}
253
254#[derive(
258 Clone,
259 Debug,
260 Serialize,
261 Deserialize,
262 Named,
263 Handler,
264 HandleClient,
265 RefClient
266)]
267pub struct WaitRankStatus {
268 pub id: ResourceId,
270 pub rank: Rank,
276 pub min_status: Status,
280 pub reply: IdleFlushPortRef<StatusOverlay>,
282}
283
284pub async fn wait_mesh<T>(
293 mut rx: PortReceiver<ValueMesh<T>>,
294 max_idle_time: Duration,
295 fallback: ValueMesh<T>,
296 reported: impl Fn(&T) -> bool,
297) -> Result<ValueMesh<T>, ValueMesh<T>>
298where
299 T: Send + Sync + Clone + 'static,
300{
301 let mut alarm = hyperactor::time::Alarm::new();
302 alarm.arm(max_idle_time);
303
304 let mut snapshot = fallback;
306
307 loop {
308 let mut sleeper = alarm.sleeper();
309 tokio::select! {
310 _ = sleeper.sleep() => return Err(snapshot),
311 next = rx.recv() => {
312 match next {
313 Ok(mesh) => { snapshot = mesh; } Err(_) => return Err(snapshot),
315 }
316 }
317 }
318
319 alarm.arm(max_idle_time);
320
321 if snapshot.values().all(|v| reported(&v)) {
323 break Ok(snapshot);
324 }
325 }
326}
327
328impl GetRankStatus {
329 pub async fn wait(
330 rx: PortReceiver<crate::StatusMesh>,
331 num_ranks: usize,
332 max_idle_time: Duration,
333 region: Region, ) -> Result<crate::StatusMesh, crate::StatusMesh> {
335 debug_assert_eq!(region.num_ranks(), num_ranks, "region/num_ranks mismatch");
336
337 let fallback = crate::StatusMesh::from_single(region, crate::resource::Status::NotExist);
338 wait_mesh(rx, max_idle_time, fallback, |s| {
339 !matches!(s, crate::resource::Status::NotExist)
340 })
341 .await
342 }
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq, Handler)]
347pub struct State<S> {
348 pub id: ResourceId,
350 pub status: Status,
352 pub state: Option<S>,
354 pub generation: u64,
356 pub timestamp: std::time::SystemTime,
358}
359wirevalue::register_type!(State<ActorState>);
360wirevalue::register_type!(State<ProcState>);
361
362impl<S: Serialize> fmt::Display for State<S> {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 match serde_json::to_string(self) {
366 Ok(json) => write!(f, "{}", json),
367 Err(e) => write!(f, "<state: serde_json error: {}>", e),
368 }
369 }
370}
371
372#[derive(
374 Debug,
375 Clone,
376 Serialize,
377 Deserialize,
378 Named,
379 Handler,
380 HandleClient,
381 RefClient
382)]
383pub struct CreateOrUpdate<S> {
384 pub id: ResourceId,
386 pub rank: Rank,
388 pub spec: S,
390}
391wirevalue::register_type!(CreateOrUpdate<ProcSpec>);
392wirevalue::register_type!(CreateOrUpdate<ActorSpec>);
393
394#[derive(
396 Debug,
397 Clone,
398 Serialize,
399 Deserialize,
400 Named,
401 Handler,
402 HandleClient,
403 RefClient
404)]
405pub struct Stop {
406 pub id: ResourceId,
408 pub reason: String,
410}
411wirevalue::register_type!(Stop);
412
413#[derive(
417 Debug,
418 Clone,
419 Serialize,
420 Deserialize,
421 Named,
422 Handler,
423 HandleClient,
424 RefClient
425)]
426pub struct StopAll {
427 pub reason: String,
429}
430wirevalue::register_type!(StopAll);
431
432#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
434#[serde(bound(serialize = "S: Named", deserialize = "S: Named"))]
435pub struct GetState<S> {
436 pub id: ResourceId,
438 #[reply]
440 pub reply: PortRef<State<S>>,
441}
442wirevalue::register_type!(GetState<ProcState>);
443wirevalue::register_type!(GetState<ActorState>);
444
445impl<S> Clone for GetState<S>
446where
447 S: RemoteMessage,
448{
449 fn clone(&self) -> Self {
450 Self {
451 id: self.id.clone(),
452 reply: self.reply.clone(),
453 }
454 }
455}
456
457#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
460#[serde(bound(serialize = "S: Named", deserialize = "S: Named"))]
461pub struct KeepaliveGetState<S> {
462 pub expires_after: std::time::SystemTime,
465 pub get_state: GetState<S>,
466}
467wirevalue::register_type!(KeepaliveGetState<ProcState>);
468wirevalue::register_type!(KeepaliveGetState<ActorState>);
469
470impl<S> Clone for KeepaliveGetState<S>
471where
472 S: RemoteMessage,
473{
474 fn clone(&self) -> Self {
475 Self {
476 expires_after: self.expires_after,
477 get_state: self.get_state.clone(),
478 }
479 }
480}
481
482#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
486#[serde(bound(serialize = "S: Named", deserialize = "S: Named"))]
487pub struct StreamState<S> {
488 pub id: ResourceId,
490 pub subscriber_rank: Rank,
494 pub subscriber: PortRef<RankedState<S>>,
496}
497wirevalue::register_type!(StreamState<ActorState>);
498wirevalue::register_type!(StreamState<ProcState>);
499
500impl<S> Clone for StreamState<S>
501where
502 S: RemoteMessage,
503{
504 fn clone(&self) -> Self {
505 Self {
506 id: self.id.clone(),
507 subscriber_rank: self.subscriber_rank.clone(),
508 subscriber: self.subscriber.clone(),
509 }
510 }
511}
512
513#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq, Handler)]
515pub struct RankedState<S> {
516 pub rank: Rank,
518 pub state: State<S>,
520}
521wirevalue::register_type!(RankedState<ActorState>);
522wirevalue::register_type!(RankedState<ProcState>);
523
524#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
526pub struct List {
527 #[reply]
529 pub reply: PortRef<Vec<ResourceId>>,
530}
531wirevalue::register_type!(List);
532
533pub trait Resource {
535 type Spec: typeuri::Named
537 + Serialize
538 + for<'de> Deserialize<'de>
539 + Send
540 + Sync
541 + std::fmt::Debug;
542
543 type State: typeuri::Named
545 + Serialize
546 + for<'de> Deserialize<'de>
547 + Send
548 + Sync
549 + std::fmt::Debug;
550}
551
552hyperactor::behavior!(
554 Controller<R: Resource>,
555 CreateOrUpdate<R::Spec>,
556 GetState<R::State>,
557 Stop,
558);
559
560#[derive(Debug, Clone, Named, Serialize, Deserialize)]
565pub struct RankedValues<T> {
566 intervals: Vec<(Range<usize>, T)>,
567}
568
569impl<T: PartialEq> PartialEq for RankedValues<T> {
570 fn eq(&self, other: &Self) -> bool {
571 self.intervals == other.intervals
572 }
573}
574
575impl<T: Eq> Eq for RankedValues<T> {}
576
577impl<T> Default for RankedValues<T> {
578 fn default() -> Self {
579 Self {
580 intervals: Vec::new(),
581 }
582 }
583}
584
585impl<T> RankedValues<T> {
586 pub fn iter(&self) -> impl Iterator<Item = &(Range<usize>, T)> + '_ {
588 self.intervals.iter()
589 }
590
591 pub fn rank(&self, value: usize) -> usize {
594 self.iter()
595 .take_while(|(ranks, _)| ranks.start <= value)
596 .map(|(ranks, _)| ranks.end.min(value) - ranks.start)
597 .sum()
598 }
599}
600
601impl<T: Clone> RankedValues<T> {
602 pub fn materialized_iter(&self, until: usize) -> impl Iterator<Item = &T> + '_ {
603 assert_eq!(self.rank(until), until, "insufficient rank");
604 self.iter()
605 .flat_map(|(range, value)| std::iter::repeat_n(value, range.end - range.start))
606 }
607}
608
609impl<T: Hash + Eq + Clone> RankedValues<T> {
610 pub fn invert(&self) -> ValuesByRank<T> {
612 let mut inverted: HashMap<T, Vec<Range<usize>>> = HashMap::new();
613 for (range, value) in self.iter() {
614 inverted
615 .entry(value.clone())
616 .or_default()
617 .push(range.clone());
618 }
619 ValuesByRank { values: inverted }
620 }
621}
622
623impl<T: Eq + Clone> RankedValues<T> {
624 pub fn merge_from(&mut self, other: Self) {
632 let mut left_iter = take(&mut self.intervals).into_iter();
633 let mut right_iter = other.intervals.into_iter();
634
635 let mut left = left_iter.next();
636 let mut right = right_iter.next();
637
638 while left.is_some() && right.is_some() {
639 let (left_ranks, left_value) = left.as_mut().unwrap();
640 let (right_ranks, right_value) = right.as_mut().unwrap();
641
642 if left_ranks.is_overlapping(right_ranks) {
643 if left_value == right_value {
644 let ranks = left_ranks.start.min(right_ranks.start)..right_ranks.end;
645 let (_, value) = replace(&mut right, right_iter.next()).unwrap();
646 left_ranks.start = ranks.end;
647 if left_ranks.is_empty() {
648 left = left_iter.next();
649 }
650 self.append(ranks, value);
651 } else if left_ranks.start < right_ranks.start {
652 let ranks = left_ranks.start..right_ranks.start;
653 left_ranks.start = ranks.end;
654 self.append(ranks, left_value.clone());
656 } else {
657 let (ranks, value) = replace(&mut right, right_iter.next()).unwrap();
658 left_ranks.start = ranks.end;
659 if left_ranks.is_empty() {
660 left = left_iter.next();
661 }
662 self.append(ranks, value);
663 }
664 } else if left_ranks.start < right_ranks.start {
665 let (ranks, value) = replace(&mut left, left_iter.next()).unwrap();
666 self.append(ranks, value);
667 } else {
668 let (ranks, value) = replace(&mut right, right_iter.next()).unwrap();
669 self.append(ranks, value);
670 }
671 }
672
673 while let Some((left_ranks, left_value)) = left {
674 self.append(left_ranks, left_value);
675 left = left_iter.next();
676 }
677 while let Some((right_ranks, right_value)) = right {
678 self.append(right_ranks, right_value);
679 right = right_iter.next();
680 }
681 }
682
683 pub fn merge_into(self, other: &mut Self) {
685 other.merge_from(self);
686 }
687
688 fn append(&mut self, range: Range<usize>, value: T) {
689 if let Some(last) = self.intervals.last_mut()
690 && last.0.end == range.start
691 && last.1 == value
692 {
693 last.0.end = range.end;
694 } else {
695 self.intervals.push((range, value));
696 }
697 }
698}
699
700impl RankedValues<Status> {
701 pub fn first_terminating(&self) -> Option<(usize, Status)> {
702 self.intervals
703 .iter()
704 .find(|(_, status)| status.is_terminating())
705 .map(|(range, status)| (range.start, status.clone()))
706 }
707
708 pub fn first_failed(&self) -> Option<(usize, Status)> {
709 self.intervals
710 .iter()
711 .find(|(_, status)| matches!(status, Status::Failed(_) | Status::Timeout(_)))
712 .map(|(range, status)| (range.start, status.clone()))
713 }
714}
715
716impl<T> From<(usize, T)> for RankedValues<T> {
717 fn from((rank, value): (usize, T)) -> Self {
718 Self {
719 intervals: vec![(rank..rank + 1, value)],
720 }
721 }
722}
723
724impl<T> From<(Range<usize>, T)> for RankedValues<T> {
725 fn from((range, value): (Range<usize>, T)) -> Self {
726 Self {
727 intervals: vec![(range, value)],
728 }
729 }
730}
731
732#[derive(Clone, Debug)]
735pub struct ValuesByRank<T> {
736 values: HashMap<T, Vec<Range<usize>>>,
737}
738
739impl<T: Eq + Hash> PartialEq for ValuesByRank<T> {
740 fn eq(&self, other: &Self) -> bool {
741 self.values == other.values
742 }
743}
744
745impl<T: Eq + Hash> Eq for ValuesByRank<T> {}
746
747impl<T: fmt::Display> fmt::Display for ValuesByRank<T> {
748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749 let mut first_value = true;
750 for (value, ranges) in self.iter() {
751 if first_value {
752 first_value = false;
753 } else {
754 write!(f, ";")?;
755 }
756 write!(f, "{}=", value)?;
757 let mut first_range = true;
758 for range in ranges.iter() {
759 if first_range {
760 first_range = false;
761 } else {
762 write!(f, ",")?;
763 }
764 write!(f, "{}..{}", range.start, range.end)?;
765 }
766 }
767 Ok(())
768 }
769}
770
771impl<T> Deref for ValuesByRank<T> {
772 type Target = HashMap<T, Vec<Range<usize>>>;
773
774 fn deref(&self) -> &Self::Target {
775 &self.values
776 }
777}
778
779impl<T> DerefMut for ValuesByRank<T> {
780 fn deref_mut(&mut self) -> &mut Self::Target {
781 &mut self.values
782 }
783}
784
785#[cfg(test)]
788impl<T> FromIterator<(Range<usize>, T)> for RankedValues<T> {
789 fn from_iter<I: IntoIterator<Item = (Range<usize>, T)>>(iter: I) -> Self {
790 Self {
791 intervals: iter.into_iter().collect(),
792 }
793 }
794}
795
796#[derive(Clone, Debug, Serialize, Deserialize, Named, Default)]
798pub(crate) struct ProcSpec {
799 pub(crate) client_config_override: Attrs,
802 pub(crate) proc_bind: Option<ProcBind>,
804 pub(crate) bootstrap_command: Option<BootstrapCommand>,
807 pub(crate) host_mesh_id: Option<crate::mesh_id::HostMeshId>,
811 pub(crate) proc_mesh_id: Option<crate::mesh_id::ProcMeshId>,
815}
816wirevalue::register_type!(ProcSpec);
817
818#[cfg(test)]
819mod tests {
820 use hyperactor::port::Port;
821
822 use super::*;
823
824 #[test]
825 fn handler_ports_are_distinct_for_resource_messages() {
826 assert_ne!(
827 Port::handler::<CreateOrUpdate<ProcSpec>>(),
828 Port::handler::<Stop>(),
829 );
830 }
831
832 #[test]
833 fn test_ranked_values_merge() {
834 #[derive(PartialEq, Debug, Eq, Clone)]
835 enum Side {
836 Left,
837 Right,
838 Both,
839 }
840 use Side::Both;
841 use Side::Left;
842 use Side::Right;
843
844 let mut left: RankedValues<Side> = [
845 (0..10, Left),
846 (15..20, Left),
847 (30..50, Both),
848 (60..70, Both),
849 ]
850 .into_iter()
851 .collect();
852
853 let right: RankedValues<Side> = [
854 (9..12, Right),
855 (25..30, Right),
856 (30..40, Both),
857 (40..50, Right),
858 (50..60, Both),
859 ]
860 .into_iter()
861 .collect();
862
863 left.merge_from(right);
864 assert_eq!(
865 left.iter().cloned().collect::<Vec<_>>(),
866 vec![
867 (0..9, Left),
868 (9..12, Right),
869 (15..20, Left),
870 (25..30, Right),
871 (30..40, Both),
872 (40..50, Right),
873 (50..70, Both)
875 ]
876 );
877
878 assert_eq!(left.rank(5), 5);
879 assert_eq!(left.rank(10), 10);
880 assert_eq!(left.rank(16), 13);
881 assert_eq!(left.rank(70), 62);
882 assert_eq!(left.rank(100), 62);
883 }
884
885 #[test]
886 fn test_equality() {
887 assert_eq!(
888 RankedValues::from((0..10, 123)),
889 RankedValues::from((0..10, 123))
890 );
891 assert_eq!(
892 RankedValues::from((0..10, Status::Failed("foo".to_string()))),
893 RankedValues::from((0..10, Status::Failed("foo".to_string()))),
894 );
895 }
896
897 #[test]
898 fn test_default_through_merging() {
899 let values: RankedValues<usize> =
900 [(0..10, 1), (15..20, 1), (30..50, 1)].into_iter().collect();
901
902 let mut default = RankedValues::from((0..50, 0));
903 default.merge_from(values);
904
905 assert_eq!(
906 default.iter().cloned().collect::<Vec<_>>(),
907 vec![
908 (0..10, 1),
909 (10..15, 0),
910 (15..20, 1),
911 (20..30, 0),
912 (30..50, 1)
913 ]
914 );
915 }
916}