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