Skip to main content

hyperactor/mailbox/
headers.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//! Message headers and latency tracking functionality for the mailbox system.
10//!
11//! This module provides header attributes and utilities for message metadata,
12//! including latency tracking timestamps used to measure message processing times.
13
14use std::any::type_name;
15use std::time::SystemTime;
16
17use hyperactor_config::Flattrs;
18use hyperactor_config::attrs::OPERATION_CONTEXT_HEADER;
19use hyperactor_config::attrs::declare_attrs;
20use hyperactor_config::global;
21use hyperactor_telemetry::hash_to_u64;
22
23use crate::ActorAddr;
24use crate::PortAddr;
25use crate::metrics::MESSAGE_LATENCY_MICROS;
26use crate::ordering::SeqInfo;
27
28declare_attrs! {
29    /// Send timestamp for message latency tracking
30    pub attr SEND_TIMESTAMP: SystemTime;
31
32    /// The rust type of the message.
33    pub attr RUST_MESSAGE_TYPE: String;
34
35    /// Hashed ActorId of the message sender, injected in post_unchecked().
36    pub attr SENDER_ACTOR_ID_HASH: u64;
37
38    /// Full ActorAddr of the session owner — the actor whose Sequencer
39    /// assigned this message's SEQ_INFO. Stamped at SEQ_INFO
40    /// assignment/install sites: MailboxExt::post, PortHandle::try_post,
41    /// and cast delivery after it installs SEQ_INFO.
42    /// Paired with the SEQ_INFO value.
43    ///
44    /// Framework-owned: stamping sites OVERWRITE caller-supplied values
45    /// (do not trust callers to know who owns the session). This attr
46    /// must never be propagated by handlers via verbatim header
47    /// forwarding; the framework will overwrite stale forwards on the
48    /// next ordered send through a trusted site.
49    ///
50    /// Larger than SENDER_ACTOR_ID_HASH (~50-100 bytes vs 8); both kept
51    /// so the hash remains available for high-cardinality OTel labels.
52    pub attr SENDER_ACTOR_ID: ActorAddr;
53
54    /// Telemetry message ID for correlating lifecycle events, injected in post_unchecked().
55    pub attr TELEMETRY_MESSAGE_ID: u64;
56
57    /// Port index the message was delivered to, injected in post_unchecked().
58    pub attr TELEMETRY_PORT_INDEX: u64;
59
60    // Operation-context headers (see `OPERATION_CONTEXT_HEADER` in
61    // `hyperactor_config::attrs`). Carried from the caller's outgoing
62    // request onto the reply envelope by a consumer-side helper that
63    // filters on `OPERATION_CONTEXT_HEADER`. Read at the
64    // undeliverable-abandonment log site in
65    // `hyperactor/src/mailbox.rs` to name the user operation a
66    // dropped reply belonged to (UM-3b).
67    //
68    // Layering note: these keys belong semantically to a higher layer
69    // (Monarch endpoint / adverb / method). They live in `hyperactor`
70    // as a tactical compromise because the log reader lives here and
71    // cannot depend upward on `monarch_hyperactor`. Scope narrowly;
72    // do not grow this vocabulary without revisiting whether the
73    // reader should move up a layer or a generic substrate-owned
74    // operation-context abstraction should replace these keys.
75
76    /// Qualified endpoint name of the caller's operation, e.g.
77    /// "<mesh>.<method>()". Stamped by the request-send site.
78    @meta(OPERATION_CONTEXT_HEADER = true)
79    pub attr OPERATION_ENDPOINT: String;
80
81    /// Endpoint adverb describing the call shape. Typical values from
82    /// current Monarch producers: "call", "call_one", "choose",
83    /// "stream".
84    @meta(OPERATION_CONTEXT_HEADER = true)
85    pub attr OPERATION_ADVERB: String;
86}
87
88// HOT PATH: Be mindful of performance when making changes here.
89// To test how performance is affected by a change, run the RPC benchmarks in
90// `monarch/python/benches/`.
91/// Set the send timestamp for latency tracking if timestamp not already set.
92pub fn set_send_timestamp(headers: &mut Flattrs) {
93    if !headers.contains_key(SEND_TIMESTAMP) {
94        let time = std::time::SystemTime::now();
95        headers.set(SEND_TIMESTAMP, time);
96    }
97}
98
99/// Set the send timestamp for latency tracking if timestamp not already set.
100pub fn set_rust_message_type<M>(headers: &mut Flattrs) {
101    headers.set(RUST_MESSAGE_TYPE, type_name::<M>().to_string());
102}
103
104/// Stamp the logical sender hash at a framework-owned delivery boundary.
105///
106/// This overwrites caller-supplied values so trusted routing state determines
107/// the sender recorded by received-message telemetry.
108#[doc(hidden)]
109pub fn stamp_sender_actor_id_hash(headers: &mut Flattrs, sender: &ActorAddr) {
110    headers.set(SENDER_ACTOR_ID_HASH, hash_to_u64(sender.id()));
111}
112
113/// Stamp `SENDER_ACTOR_ID` into `headers` if the gate conditions are met.
114/// Framework-owned: overwrites existing values, never "sets if absent".
115///
116/// Gate: stamp when (early-session OR caller-set-stale) AND ordered handler
117/// traffic. `caller_set_stale` defends against handlers that forward inbound
118/// headers verbatim.
119///
120/// Callable from cross-crate cast-delivery sites, so visibility is `pub` with
121/// `#[doc(hidden)]` to keep it out of the public API surface.
122#[doc(hidden)]
123pub fn stamp_sender_actor_id(
124    headers: &mut Flattrs,
125    seq_info: &SeqInfo,
126    dest: &PortAddr,
127    owner: &ActorAddr,
128) {
129    if let SeqInfo::Session { seq, .. } = seq_info {
130        let early_session = *seq <= 4;
131        let caller_set_stale = headers.contains_key(SENDER_ACTOR_ID);
132        if (early_session || caller_set_stale) && dest.is_handler_port() {
133            headers.set(SENDER_ACTOR_ID, owner.clone());
134        }
135    }
136}
137
138/// Simpler stamping for paths where headers start fresh (no caller-supplied
139/// stale value to defend against). Used only within the hyperactor crate by
140/// PortHandle::try_post.
141pub(crate) fn stamp_sender_actor_id_fresh(
142    headers: &mut Flattrs,
143    seq: u64,
144    dest: &PortAddr,
145    owner: &ActorAddr,
146) {
147    if seq <= 4 && dest.is_handler_port() {
148        headers.set(SENDER_ACTOR_ID, owner.clone());
149    }
150}
151
152// HOT PATH: Be mindful of performance when making changes here.
153// To test how performance is affected by a change, run the RPC benchmarks in
154// `monarch/python/benches/`.
155/// This function checks the configured sampling rate and, if the random sample passes,
156/// calculates the latency between the send timestamp and the current time, then records
157/// the latency metric with the associated actor ID.
158pub fn log_message_latency_if_sampling(headers: &Flattrs, actor_addr: &ActorAddr) {
159    if fastrand::f32() > global::get(crate::config::MESSAGE_LATENCY_SAMPLING_RATE) {
160        return;
161    }
162
163    let actor_id = actor_addr.to_string();
164    if !headers.contains_key(SEND_TIMESTAMP) {
165        tracing::debug!(
166            actor_id = actor_id,
167            "SEND_TIMESTAMP missing from message headers, cannot measure latency"
168        );
169        return;
170    }
171
172    let metric_pairs = hyperactor_telemetry::kv_pairs!(
173        "actor_id" => actor_id
174    );
175    let Some(send_timestamp) = headers.get(SEND_TIMESTAMP) else {
176        return;
177    };
178    let now = std::time::SystemTime::now();
179    let latency = now.duration_since(send_timestamp).unwrap_or_default();
180    MESSAGE_LATENCY_MICROS.record(latency.as_micros() as f64, metric_pairs);
181}
182
183#[cfg(test)]
184mod tests {
185    use uuid::Uuid;
186
187    use super::*;
188    use crate::port::ControlPort;
189    use crate::port::Port;
190    use crate::testing::ids::test_actor_id;
191
192    fn session(seq: u64) -> SeqInfo {
193        SeqInfo::Session {
194            session_id: Uuid::now_v7(),
195            seq,
196        }
197    }
198
199    fn handler_port(actor_name: &str) -> (ActorAddr, PortAddr) {
200        let addr: ActorAddr = test_actor_id(actor_name, "worker");
201        let port = addr.port_addr(Port::handler::<TestHandlerMsg>());
202        (addr, port)
203    }
204
205    fn non_handler_port(actor_name: &str) -> (ActorAddr, PortAddr) {
206        let addr: ActorAddr = test_actor_id(actor_name, "worker");
207        // Non-handler port: a plain Port::from(integer), without the handler
208        // bit. Per the ordering tests (test_sequencer_non_handler_ports_*),
209        // Port::from(N) for small N is a non-handler port.
210        let port = addr.port_addr(Port::from(1));
211        (addr, port)
212    }
213
214    fn control_port(actor_name: &str) -> (ActorAddr, PortAddr) {
215        let addr: ActorAddr = test_actor_id(actor_name, "worker");
216        let port = addr.port_addr(Port::control(ControlPort::Introspect));
217        (addr, port)
218    }
219
220    // A test handler-port message type. Named with handler-port semantics
221    // so its port is a handler port distinct from bypass ports.
222    #[derive(typeuri::Named)]
223    struct TestHandlerMsg;
224
225    #[test]
226    fn test_stamp_sender_actor_id_hash_overwrites_existing_value() {
227        let owner: ActorAddr = test_actor_id("test_0", "worker");
228        let mut headers = Flattrs::new();
229        headers.set(SENDER_ACTOR_ID_HASH, 123);
230
231        stamp_sender_actor_id_hash(&mut headers, &owner);
232
233        assert_eq!(
234            headers.get(SENDER_ACTOR_ID_HASH),
235            Some(hash_to_u64(owner.id()))
236        );
237    }
238
239    #[test]
240    fn test_stamp_helper_sets_sender_on_seq_1() {
241        let (owner, dest) = handler_port("test_0");
242        let mut headers = Flattrs::new();
243        stamp_sender_actor_id(&mut headers, &session(1), &dest, &owner);
244        assert_eq!(headers.get(SENDER_ACTOR_ID), Some(owner));
245    }
246
247    #[test]
248    fn test_stamp_helper_sets_sender_on_seq_4() {
249        let (owner, dest) = handler_port("test_0");
250        let mut headers = Flattrs::new();
251        stamp_sender_actor_id(&mut headers, &session(4), &dest, &owner);
252        assert_eq!(headers.get(SENDER_ACTOR_ID), Some(owner));
253    }
254
255    #[test]
256    fn test_stamp_helper_skips_seq_5_no_stale() {
257        let (owner, dest) = handler_port("test_0");
258        let mut headers = Flattrs::new();
259        stamp_sender_actor_id(&mut headers, &session(5), &dest, &owner);
260        assert_eq!(headers.get(SENDER_ACTOR_ID), None);
261    }
262
263    #[test]
264    fn test_stamp_helper_overwrites_stale_at_seq_5() {
265        let (owner, dest) = handler_port("test_0");
266        let fake_owner: ActorAddr = test_actor_id("fake_0", "imposter");
267        let mut headers = Flattrs::new();
268        headers.set(SENDER_ACTOR_ID, fake_owner.clone());
269        stamp_sender_actor_id(&mut headers, &session(5), &dest, &owner);
270        assert_eq!(headers.get(SENDER_ACTOR_ID), Some(owner));
271    }
272
273    #[test]
274    fn test_stamp_helper_skips_non_handler_port() {
275        let (owner, dest) = non_handler_port("test_0");
276        let mut headers = Flattrs::new();
277        stamp_sender_actor_id(&mut headers, &session(1), &dest, &owner);
278        assert_eq!(headers.get(SENDER_ACTOR_ID), None);
279    }
280
281    #[test]
282    fn test_stamp_helper_skips_control_port() {
283        let (owner, dest) = control_port("test_0");
284        let mut headers = Flattrs::new();
285        stamp_sender_actor_id(&mut headers, &session(1), &dest, &owner);
286        assert_eq!(headers.get(SENDER_ACTOR_ID), None);
287    }
288
289    #[test]
290    fn test_stamp_helper_skips_seq_info_direct() {
291        let (owner, dest) = handler_port("test_0");
292        let mut headers = Flattrs::new();
293        stamp_sender_actor_id(&mut headers, &SeqInfo::Direct, &dest, &owner);
294        assert_eq!(headers.get(SENDER_ACTOR_ID), None);
295    }
296
297    #[test]
298    fn test_stamp_fresh_helper_sets_on_seq_4() {
299        let (owner, dest) = handler_port("test_0");
300        let mut headers = Flattrs::new();
301        stamp_sender_actor_id_fresh(&mut headers, 4, &dest, &owner);
302        assert_eq!(headers.get(SENDER_ACTOR_ID), Some(owner));
303    }
304
305    #[test]
306    fn test_stamp_fresh_helper_skips_on_seq_5() {
307        let (owner, dest) = handler_port("test_0");
308        let mut headers = Flattrs::new();
309        stamp_sender_actor_id_fresh(&mut headers, 5, &dest, &owner);
310        assert_eq!(headers.get(SENDER_ACTOR_ID), None);
311    }
312}