Skip to main content

hyperactor_mesh/
resource.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//! This modules defines a set of common message types used for managing resources
10//! in hyperactor meshes.
11//!
12//! # Rank-status positioning (`RSP-*`)
13//!
14//! Status queries ([`GetRankStatus`], [`WaitRankStatus`]) reply with a
15//! [`StatusOverlay`] — a sparse, rank-indexed patch the caller merges into an
16//! accumulator over its own view. Positioning that overlay is the contract this
17//! registry names; each invariant is cited at the sites that must uphold it.
18//!
19//! - **RSP-1** — an overlay is positioned at the recipient's dense rank in the
20//!   *consuming* view (the one the caller accumulates over), never the rank the
21//!   resource was first created under. Reusing the creation rank mis-attributes
22//!   or drops the reply over a renumbered or sliced view.
23//! - **RSP-2** — the positioning rank travels as the [`Rank`] message field. Cast
24//!   sites leave it `Rank::default()` and the cast layer late-binds it in transit
25//!   with the delivered dense rank; direct (non-cast) delivery supplies it
26//!   explicitly with `Rank::new(view_rank)`.
27//! - **RSP-3** — a deferred waiter (a [`WaitRankStatus`] below its threshold, or
28//!   one registered before its resource exists) retains its registration rank
29//!   until it is flushed; the flush never rewrites it to a creation rank.
30//! - **RSP-4** — when a handler reports absence *immediately*, it returns an
31//!   empty overlay; the message's [`Rank`] is never overloaded as an absence
32//!   signal. `WaitRankStatus` may instead defer when later creation is expected
33//!   (RSP-3), replying once the resource exists.
34//!
35//! `ActorState` streaming supervision must satisfy the analogous positioning
36//! contract; extending this registry to cover `ActorState` is future work.
37
38pub 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/// The current lifecycle status of a resource.
78#[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    /// The resource does not exist.
94    NotExist,
95    /// The resource is being created.
96    Initializing,
97    /// The resource is running.
98    Running,
99    /// The resource is being stopped.
100    Stopping,
101    /// The resource is stopped.
102    Stopped,
103    /// The resource has failed, with an error message.
104    #[strum(to_string = "Failed({0})")]
105    Failed(String),
106    /// The resource has been declared failed after a timeout.
107    #[strum(to_string = "Timeout({0:?})")]
108    Timeout(Duration),
109    /// The resource exists but its status is not known.
110    Unknown,
111}
112
113impl Status {
114    /// Returns whether the status is a terminating status (includes `Stopping`).
115    pub fn is_terminating(&self) -> bool {
116        matches!(
117            self,
118            Status::Stopping | Status::Stopped | Status::Failed(_) | Status::Timeout(_)
119        )
120    }
121
122    /// Tells whether the status represents a failure. A failure is both terminating
123    /// (the resource is not running), but also means abnormal exit (the resource
124    /// did not stop cleanly).
125    pub fn is_failure(&self) -> bool {
126        matches!(self, Self::Failed(_) | Self::Timeout(_))
127    }
128
129    /// Returns whether the status is fully terminal (the resource has
130    /// stopped, failed, or timed out — but NOT merely `Stopping`).
131    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    /// Ensure this status is at least as terminal as `floor`.
143    ///
144    /// If `floor` is a terminating status (Stopping, Stopped, Failed,
145    /// Timeout) and `self` is not, returns `floor`. Otherwise returns
146    /// `self` unchanged. This is used to prevent a child resource
147    /// from appearing healthier than its parent.
148    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/// Data type used to communicate ranks.
181/// Serialized as a typed multipart part so comm actors can replace instances
182/// with the delivered rank while the message is in transit.
183#[derive(Clone, Debug, Named, PartialEq, Eq, Default)]
184pub struct Rank(pub Option<usize>);
185wirevalue::register_type!(Rank);
186
187/// Serialized representation for [`Rank`] multipart parts.
188#[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    /// Create a new rank with the provided value.
216    pub fn new(rank: usize) -> Self {
217        Self(Some(rank))
218    }
219
220    /// Unwrap the rank; panics if not set.
221    pub fn unwrap(&self) -> usize {
222        self.0.unwrap()
223    }
224}
225
226/// Get the status of a resource.
227///
228/// Delivered either by cast to every rank or by direct point-to-point send to a
229/// chosen recipient. Each recipient replies with a sparse status **overlay**.
230/// The comm reducer merges overlays (right-wins) and the accumulator applies
231/// them to produce **full StatusMesh snapshots** on the receiver side.
232#[derive(
233    Clone,
234    Debug,
235    Serialize,
236    Deserialize,
237    Named,
238    Handler,
239    HandleClient,
240    RefClient
241)]
242pub struct GetRankStatus {
243    /// The resource identifier.
244    pub id: ResourceId,
245    /// The rank at which the recipient positions its reply overlay, in the
246    /// caller's view (RSP-1, RSP-2). Cast sites leave this `Rank::default()` and
247    /// the cast layer rewrites it in transit with the delivered dense rank;
248    /// direct (non-cast) callers set it explicitly with `Rank::new(...)`.
249    pub rank: Rank,
250    /// Sparse status updates (overlays) from a rank.
251    pub reply: IdleFlushPortRef<StatusOverlay>,
252}
253
254/// Like [`GetRankStatus`], but the handler defers its reply until the
255/// resource's status is >= `min_status`. This avoids the race where
256/// the caller sees `Stopping` before the process has actually exited.
257#[derive(
258    Clone,
259    Debug,
260    Serialize,
261    Deserialize,
262    Named,
263    Handler,
264    HandleClient,
265    RefClient
266)]
267pub struct WaitRankStatus {
268    /// The resource identifier.
269    pub id: ResourceId,
270    /// The rank at which the recipient positions its reply overlay, in the
271    /// caller's view (RSP-1, RSP-2). Cast sites leave this `Rank::default()` and
272    /// the cast layer rewrites it in transit with the delivered dense rank;
273    /// direct (non-cast) callers set it explicitly with `Rank::new(...)`. A
274    /// deferred waiter retains this rank until it is flushed (RSP-3).
275    pub rank: Rank,
276    /// The minimum status the caller wants to observe.
277    /// The handler will not reply until the resource's status
278    /// is >= this threshold.
279    pub min_status: Status,
280    /// Sparse status updates (overlays) from a rank.
281    pub reply: IdleFlushPortRef<StatusOverlay>,
282}
283
284/// Collect an accumulated [`ValueMesh<T>`] from `rx` until every rank has been
285/// reported or `max_idle_time` elapses with no update.
286///
287/// `reported` decides whether a rank's current value counts as "reported" (e.g.
288/// moved off a `NotExist`/`Timeout` placeholder). The accumulator emits the full
289/// mesh over the target region, so completion is "every cell reported". Returns
290/// `Ok(mesh)` once all ranks are reported, or `Err(mesh)` carrying the latest
291/// snapshot (or `fallback` if nothing arrived) on idle timeout / channel close.
292pub 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    // Latest-wins snapshot; `fallback` stands in until the first update arrives.
305    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; }   // latest-wins snapshot
314                    Err(_)   => return Err(snapshot),
315                }
316            }
317        }
318
319        alarm.arm(max_idle_time);
320
321        // Short-circuit: done as soon as no rank is still unreported.
322        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, // used only for fallback
334    ) -> 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/// The state of a resource.
346#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq, Handler)]
347pub struct State<S> {
348    /// The resource identifier.
349    pub id: ResourceId,
350    /// Its status.
351    pub status: Status,
352    /// Optionally, a resource-defined state.
353    pub state: Option<S>,
354    /// Monotonic generation counter for last-writer-wins ordering.
355    pub generation: u64,
356    /// Wall-clock timestamp for debugging context.
357    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        // Use serde_json to serialize the struct to a compact JSON string
365        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/// Create or update a resource according to a spec.
373#[derive(
374    Debug,
375    Clone,
376    Serialize,
377    Deserialize,
378    Named,
379    Handler,
380    HandleClient,
381    RefClient
382)]
383pub struct CreateOrUpdate<S> {
384    /// The resource identifier.
385    pub id: ResourceId,
386    /// The rank of the resource, when available.
387    pub rank: Rank,
388    /// The specification of the resource.
389    pub spec: S,
390}
391wirevalue::register_type!(CreateOrUpdate<ProcSpec>);
392wirevalue::register_type!(CreateOrUpdate<ActorSpec>);
393
394/// Stop a resource according to a spec.
395#[derive(
396    Debug,
397    Clone,
398    Serialize,
399    Deserialize,
400    Named,
401    Handler,
402    HandleClient,
403    RefClient
404)]
405pub struct Stop {
406    /// The resource identifier.
407    pub id: ResourceId,
408    /// The reason for stopping the resource.
409    pub reason: String,
410}
411wirevalue::register_type!(Stop);
412
413/// Stop all resources owned by the receiver of this message.
414/// No reply, this just issues the stop command.
415/// Use GetRankStatus to determine if it has successfully stopped.
416#[derive(
417    Debug,
418    Clone,
419    Serialize,
420    Deserialize,
421    Named,
422    Handler,
423    HandleClient,
424    RefClient
425)]
426pub struct StopAll {
427    /// The reason for stopping.
428    pub reason: String,
429}
430wirevalue::register_type!(StopAll);
431
432/// Retrieve the current state of the resource.
433#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
434#[serde(bound(serialize = "S: Named", deserialize = "S: Named"))]
435pub struct GetState<S> {
436    /// The resource identifier.
437    pub id: ResourceId,
438    /// A reply containing the state.
439    #[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/// Same as GetState, but additionally tells the receiver that the owner is still alive.
458/// If the receiver does not receive this message for a while, it might assume the owner is dead.
459#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
460#[serde(bound(serialize = "S: Named", deserialize = "S: Named"))]
461pub struct KeepaliveGetState<S> {
462    /// The time at which the actor should be considered expired if no further
463    /// keepalive is received.
464    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/// Subscribe to streaming state updates for a named resource.
483/// The subscriber port will receive `RankedState<S>` whenever the resource's
484/// state changes. The current state is sent immediately upon subscription.
485#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
486#[serde(bound(serialize = "S: Named", deserialize = "S: Named"))]
487pub struct StreamState<S> {
488    /// The resource identifier.
489    pub id: ResourceId,
490    /// The recipient's rank in the subscriber's view. Cast callers leave this
491    /// unset so the comm layer can fill it for each recipient; direct callers
492    /// set it explicitly.
493    pub subscriber_rank: Rank,
494    /// A streaming port that will receive state updates.
495    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/// A state update positioned in the subscriber's current view.
514#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq, Handler)]
515pub struct RankedState<S> {
516    /// The resource's rank in the consuming view.
517    pub rank: Rank,
518    /// The state observed at that rank.
519    pub state: State<S>,
520}
521wirevalue::register_type!(RankedState<ActorState>);
522wirevalue::register_type!(RankedState<ProcState>);
523
524/// List the set of resources managed by the controller.
525#[derive(Debug, Serialize, Deserialize, Named, Handler, HandleClient, RefClient)]
526pub struct List {
527    /// List of resource names managed by this controller.
528    #[reply]
529    pub reply: PortRef<Vec<ResourceId>>,
530}
531wirevalue::register_type!(List);
532
533/// A trait that bundles a set of types that together define a resource.
534pub trait Resource {
535    /// The spec specification for this resource.
536    type Spec: typeuri::Named
537        + Serialize
538        + for<'de> Deserialize<'de>
539        + Send
540        + Sync
541        + std::fmt::Debug;
542
543    /// The state for this resource.
544    type State: typeuri::Named
545        + Serialize
546        + for<'de> Deserialize<'de>
547        + Send
548        + Sync
549        + std::fmt::Debug;
550}
551
552// A behavior defining the interface for a mesh controller.
553hyperactor::behavior!(
554    Controller<R: Resource>,
555    CreateOrUpdate<R::Spec>,
556    GetState<R::State>,
557    Stop,
558);
559
560/// RankedValues compactly represents rank-indexed values of type T.
561/// It stores contiguous values in a set of intervals; thus it is
562/// efficient and compact when the cardinality of T-typed values is
563/// low.
564#[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    /// Iterate over contiguous rank intervals of values.
587    pub fn iter(&self) -> impl Iterator<Item = &(Range<usize>, T)> + '_ {
588        self.intervals.iter()
589    }
590
591    /// The (set) rank of the RankedValues is the number of values stored with
592    /// rank less than `value`.
593    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    /// Invert this ranked values into a [`ValuesByRank<T>`].
611    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    /// Merge `other` into this set of ranked values. Values in `other` that overlap
625    /// with `self` take prededence.
626    ///
627    /// This currently uses a simple algorithm that merges the full set of RankedValues.
628    /// This remains efficient when the cardinality of T-typed values is low. However,
629    /// it does not efficiently merge high cardinality value sets. Consider using interval
630    /// trees or bitmap techniques like Roaring Bitmaps in these cases.
631    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                    // TODO: get rid of clone
655                    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    /// Merge the contents of this RankedValues into another RankedValues.
684    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/// An inverted index of RankedValues, providing all ranks for
733/// which each unique T-typed value appears.
734#[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/// Enabled for test only because we have to guarantee that the input
786/// iterator is well-formed.
787#[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/// Spec for a host mesh agent to use when spawning a new proc.
797#[derive(Clone, Debug, Serialize, Deserialize, Named, Default)]
798pub(crate) struct ProcSpec {
799    /// Config values to set on the spawned proc's global config,
800    /// at the `ClientOverride` layer.
801    pub(crate) client_config_override: Attrs,
802    /// Optional per-process CPU/NUMA binding configuration.
803    pub(crate) proc_bind: Option<ProcBind>,
804    /// Optional bootstrap command override. When set, this command is used
805    /// to spawn the proc instead of the host agent's default bootstrap command.
806    pub(crate) bootstrap_command: Option<BootstrapCommand>,
807    /// The id of the HostMesh that owns this proc. Used by
808    /// `DrainHost` to selectively drain only procs belonging to a
809    /// specific mesh.
810    pub(crate) host_mesh_id: Option<crate::mesh_id::HostMeshId>,
811    /// The id of the ProcMesh that owns this proc. A host agent can hold procs
812    /// from several proc meshes, so this lets per-mesh queries (e.g.
813    /// `StreamState`) scope to a single mesh rather than every proc on the host.
814    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                // Merge consecutive:
874                (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}