Skip to main content

monarch_hyperactor/
operation_context.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//! Operation-context carrier for the Python endpoint machinery.
10//!
11//! Stamps `OPERATION_ENDPOINT` / `OPERATION_ADVERB` on outgoing
12//! requests so failure surfaces (mailbox log, undeliverable
13//! formatter) can name the operation.
14//!
15//! ## Operation-context invariants (OC-*)
16//!
17//! - **OC-1 (request-originated).** Stamped at the caller's
18//!   request-send site. Only the caller knows the qualified
19//!   endpoint and adverb.
20//!
21//! - **OC-2 (filtered carry).** Only keys marked
22//!   `@meta(OPERATION_CONTEXT_HEADER = true)` ride this path.
23//!   Adding another `OPERATION_*` key joins the vocabulary by
24//!   declaration alone.
25//!
26//! - **OC-3 (carry continuity).** Captured headers are preserved
27//!   onto the reply envelope at the `Port` capture site via
28//!   `send_with_headers`.
29
30use hyperactor::mailbox::headers::OPERATION_ADVERB;
31use hyperactor::mailbox::headers::OPERATION_ENDPOINT;
32use hyperactor_config::Attrs;
33use hyperactor_config::Flattrs;
34use hyperactor_config::attrs::OPERATION_CONTEXT_HEADER;
35use hyperactor_config::attrs::stamp_marked_attrs_into_flattrs;
36
37/// Build an `Attrs` carrier populated with the operation-context keys
38/// an endpoint-send site wants to stamp onto its outgoing request.
39///
40/// Callers supply the values they know in scope at the send site
41/// (qualified endpoint name, adverb token). Any `None` is simply
42/// omitted — the marker-driven stamp filters on presence.
43pub fn build_operation_context_attrs(
44    endpoint: Option<String>,
45    adverb: Option<&'static str>,
46) -> Attrs {
47    let mut attrs = Attrs::new();
48    if let Some(ep) = endpoint {
49        attrs.set(OPERATION_ENDPOINT, ep);
50    }
51    if let Some(a) = adverb {
52        attrs.set(OPERATION_ADVERB, a.to_string());
53    }
54    attrs
55}
56
57/// Stamp the operation-context subset of `attrs` onto `headers` using
58/// the shared marker-driven mechanism. Only entries whose declared
59/// key carries `@meta(OPERATION_CONTEXT_HEADER = true)` are written;
60/// anything else in `attrs` is silently skipped (OC-2).
61pub fn stamp_operation_context(headers: &mut Flattrs, attrs: &Attrs) {
62    stamp_marked_attrs_into_flattrs(headers, attrs, OPERATION_CONTEXT_HEADER);
63}
64
65#[cfg(test)]
66mod tests {
67    use hyperactor_config::attrs::copy_marked_flattrs;
68
69    use super::*;
70
71    /// OC-2: the shared marker-driven copy propagates exactly the
72    /// `OPERATION_CONTEXT_HEADER`-marked keys from the source headers
73    /// onto the destination. Unrelated declared `Flattrs` entries
74    /// (e.g. `SEND_TIMESTAMP`) are excluded even though they're
75    /// present on the source.
76    #[test]
77    fn test_oc2_filter_rejects_unmarked_keys() {
78        let mut src = Flattrs::new();
79        src.set(OPERATION_ENDPOINT, "training.buffer.sample()".to_string());
80        src.set(
81            hyperactor::mailbox::headers::SEND_TIMESTAMP,
82            std::time::SystemTime::UNIX_EPOCH,
83        );
84
85        let mut dst = Flattrs::new();
86        copy_marked_flattrs(&mut dst, &src, OPERATION_CONTEXT_HEADER);
87
88        assert_eq!(
89            dst.get(OPERATION_ENDPOINT),
90            Some("training.buffer.sample()".to_string()),
91            "OC-2: marked key must cross the carry"
92        );
93        assert!(
94            dst.get(hyperactor::mailbox::headers::SEND_TIMESTAMP)
95                .is_none(),
96            "OC-2: unmarked key must not cross the carry"
97        );
98    }
99
100    /// OC-1: the caller-side helper assembles the operation-context
101    /// attrs from values known at the request-send site, rather
102    /// than synthesized at the reply site. `build_operation_context_attrs`
103    /// is the vehicle for that origination step.
104    #[test]
105    fn test_oc1_build_attrs_populates_supplied_fields() {
106        let attrs = build_operation_context_attrs(
107            Some("training.buffer.sample()".to_string()),
108            Some("call_one"),
109        );
110        assert_eq!(
111            attrs.get(OPERATION_ENDPOINT),
112            Some(&"training.buffer.sample()".to_string())
113        );
114        assert_eq!(attrs.get(OPERATION_ADVERB), Some(&"call_one".to_string()));
115    }
116
117    /// OC-1: fields the request site cannot supply must not be
118    /// fabricated downstream. `build_operation_context_attrs` omits
119    /// them when the caller passes `None`.
120    #[test]
121    fn test_oc1_build_attrs_omits_none_fields() {
122        let attrs =
123            build_operation_context_attrs(Some("training.buffer.sample()".to_string()), None);
124        assert_eq!(
125            attrs.get(OPERATION_ENDPOINT),
126            Some(&"training.buffer.sample()".to_string())
127        );
128        assert!(attrs.get(OPERATION_ADVERB).is_none());
129    }
130
131    /// OC-2: the stamp helper writes only the
132    /// `OPERATION_CONTEXT_HEADER`-marked keys onto the outgoing
133    /// headers. Same mechanism as the OC-2 copy test above, on the
134    /// `Attrs -> Flattrs` direction.
135    #[test]
136    fn test_oc2_stamp_writes_marked_keys_to_headers() {
137        let attrs = build_operation_context_attrs(
138            Some("training.buffer.sample()".to_string()),
139            Some("call_one"),
140        );
141        let mut headers = Flattrs::new();
142        stamp_operation_context(&mut headers, &attrs);
143
144        assert_eq!(
145            headers.get(OPERATION_ENDPOINT),
146            Some("training.buffer.sample()".to_string())
147        );
148        assert_eq!(headers.get(OPERATION_ADVERB), Some("call_one".to_string()));
149    }
150}