Skip to main content

aion_server/worker/
declared_body_selection.rs

1//! Which retained version's body a declared-command dispatch runs.
2//!
3//! Content-hash namespacing keeps every deployed version of a document alive at
4//! once, and a run is pinned to exactly one of them for its whole life — that is
5//! the mechanism that lets a workflow sleep for three months and wake up as
6//! itself. So the question "what command does this action run?" has a single
7//! correct answer, and it is not a property of the queue: **it is the body the
8//! DISPATCHING RUN's own package version declares.**
9//!
10//! Resolution used to scan every retained version on the task queue and refuse
11//! the dispatch when two of them declared different bodies. That reading makes
12//! a second deploy fatal to work already in flight: a run started under version
13//! A, still executing, fails terminally at its next bodied activity because
14//! version B — which that run will never execute — was deployed with an edited
15//! command. Nothing about the run changed. Under the correct reading there is
16//! no conflict to refuse: A's runs use A's body, B's runs use B's.
17//!
18//! When the run's version cannot be named — its handle is gone from the
19//! registry, or the registry could not be read — the queue-wide reading is used
20//! instead, ambiguity refusal and all. Without the run's identity, choosing a
21//! body IS the guess the refusal exists to prevent.
22
23use aion::DeployedWorkerContract;
24use aion_package::{ActionBodyContract, ContentHash};
25
26use super::declared_body::DeclaredBodyLookup;
27use super::declared_body_ambiguity::DeclaringVersion;
28
29/// Resolve the body `action` runs, given every retained contract on the queue
30/// and — when it could be named — the package version the dispatching run is
31/// pinned to.
32///
33/// With `run_version` known, that version's contract is the SOLE authority: if
34/// it declares a body, that body runs; if it does not, the action belongs to an
35/// out-of-band worker and the dispatch is delegated. Reading a body off any
36/// other version would run a command the run's own package never declared.
37#[must_use]
38pub fn select_declared_body(
39    contracts: &[DeployedWorkerContract],
40    action: &str,
41    run_version: Option<&ContentHash>,
42) -> DeclaredBodyLookup {
43    match run_version {
44        Some(version) => body_of_version(contracts, action, version),
45        None => body_across_queue(contracts, action),
46    }
47}
48
49/// The body `version` itself declares for `action`, or
50/// [`DeclaredBodyLookup::None`] when it declares none — including when that
51/// version does not serve this queue at all.
52fn body_of_version(
53    contracts: &[DeployedWorkerContract],
54    action: &str,
55    version: &ContentHash,
56) -> DeclaredBodyLookup {
57    for deployed in contracts {
58        if &deployed.package_version != version {
59            continue;
60        }
61        for declared in &deployed.contract.actions {
62            if declared.name == action
63                && let Some(body) = &declared.body
64            {
65                return DeclaredBodyLookup::Declared(body.clone());
66            }
67        }
68    }
69    DeclaredBodyLookup::None
70}
71
72/// The queue-wide reading, used only when the run's version is unknown.
73///
74/// Identical bodies across versions collapse to one; different bodies refuse,
75/// carrying the declaring versions so the refusal can name what to retire.
76fn body_across_queue(contracts: &[DeployedWorkerContract], action: &str) -> DeclaredBodyLookup {
77    let mut bodies: Vec<ActionBodyContract> = Vec::new();
78    let mut declaring: Vec<DeclaringVersion> = Vec::new();
79    for deployed in contracts {
80        for declared in &deployed.contract.actions {
81            if declared.name == action
82                && let Some(body) = &declared.body
83            {
84                let index = bodies
85                    .iter()
86                    .position(|seen| seen == body)
87                    .unwrap_or_else(|| {
88                        bodies.push(body.clone());
89                        bodies.len() - 1
90                    });
91                declaring.push(DeclaringVersion {
92                    content_hash: deployed.package_version.to_string(),
93                    workflow_types: deployed.workflow_types.clone(),
94                    route_active: deployed.route_active,
95                    body: index,
96                });
97                // One record per retained version. A contract names each action
98                // once, and stopping here keeps the version count in the refusal
99                // equal to the number of versions an operator would act on.
100                break;
101            }
102        }
103    }
104    match bodies.len() {
105        0 => DeclaredBodyLookup::None,
106        1 => match bodies.pop() {
107            Some(body) => DeclaredBodyLookup::Declared(body),
108            // Unreachable by the length check; classified rather than panicked
109            // so a refactor cannot turn this into an abort.
110            None => DeclaredBodyLookup::None,
111        },
112        _ => DeclaredBodyLookup::Ambiguous { declaring },
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use aion::DeployedWorkerContract;
119    use aion_package::{ActionBodyContract, ActionContract, ContentHash, WorkerContract};
120
121    use super::super::declared_body::DeclaredBodyLookup;
122    use super::select_declared_body;
123
124    fn version(byte: u8) -> ContentHash {
125        ContentHash::from_bytes([byte; 32])
126    }
127
128    /// One retained version declaring `action` with `command`, or with no body
129    /// at all when `command` is `None`.
130    fn contract(
131        hash: u8,
132        workflow_type: &str,
133        route_active: bool,
134        action: &str,
135        command: Option<&str>,
136    ) -> DeployedWorkerContract {
137        DeployedWorkerContract {
138            package_version: version(hash),
139            contract: WorkerContract {
140                task_queue: "local".to_owned(),
141                actions: vec![ActionContract {
142                    name: action.to_owned(),
143                    input_schema: serde_json::json!({"type": "object"}),
144                    output_schema: serde_json::json!({"type": "object"}),
145                    node: None,
146                    timeout: None,
147                    retry: None,
148                    advisory: false,
149                    agent: false,
150                    body: command.map(|command| ActionBodyContract::Run {
151                        command: command.to_owned(),
152                    }),
153                }],
154            },
155            workflow_types: vec![workflow_type.to_owned()],
156            route_active,
157        }
158    }
159
160    fn command_of(lookup: &DeclaredBodyLookup) -> Option<&str> {
161        match lookup {
162            DeclaredBodyLookup::Declared(ActionBodyContract::Run { command }) => Some(command),
163            _ => None,
164        }
165    }
166
167    /// The defect this module exists to remove: a run pinned to the OLD version
168    /// keeps running the old version's command after a new one is deployed. The
169    /// queue-wide reading refused the dispatch outright.
170    #[test]
171    fn an_in_flight_run_keeps_its_own_versions_body_after_a_redeploy() {
172        let retained = [
173            contract(0x0a, "sweep", false, "collect", Some("echo old")),
174            contract(0x0b, "sweep", true, "collect", Some("echo new")),
175        ];
176
177        let old_run = select_declared_body(&retained, "collect", Some(&version(0x0a)));
178        assert_eq!(
179            command_of(&old_run),
180            Some("echo old"),
181            "the run pinned to the superseded version must keep its own body: {old_run:?}"
182        );
183
184        let new_run = select_declared_body(&retained, "collect", Some(&version(0x0b)));
185        assert_eq!(
186            command_of(&new_run),
187            Some("echo new"),
188            "a run on the route-active version gets the new body: {new_run:?}"
189        );
190
191        // The control: the SAME retained set, read without the run's identity,
192        // is the refusal this fix removes. If this ever stops refusing, the two
193        // assertions above stop proving anything about the fix.
194        let blind = select_declared_body(&retained, "collect", None);
195        assert!(
196            matches!(blind, DeclaredBodyLookup::Ambiguous { .. }),
197            "without the run's version the queue-wide reading must still refuse: {blind:?}"
198        );
199    }
200
201    /// A run's own version is the sole authority. Where it declares no body,
202    /// the action is a requirement on a worker — even though another retained
203    /// version happens to declare one.
204    #[test]
205    fn a_version_without_a_body_delegates_rather_than_borrowing_one() {
206        let retained = [
207            contract(0x0a, "sweep", false, "collect", None),
208            contract(0x0b, "sweep", true, "collect", Some("echo new")),
209        ];
210        let lookup = select_declared_body(&retained, "collect", Some(&version(0x0a)));
211        assert!(
212            matches!(lookup, DeclaredBodyLookup::None),
213            "a run whose package declares no body must delegate, not borrow: {lookup:?}"
214        );
215    }
216
217    /// A version that does not serve this queue at all is not a body source
218    /// either — the same rule, reached by a different route.
219    #[test]
220    fn a_version_absent_from_the_queue_declares_nothing_here() {
221        let retained = [contract(0x0b, "sweep", true, "collect", Some("echo new"))];
222        let lookup = select_declared_body(&retained, "collect", Some(&version(0x0c)));
223        assert!(
224            matches!(lookup, DeclaredBodyLookup::None),
225            "an unrelated version cannot pick up this queue's body: {lookup:?}"
226        );
227    }
228
229    /// Agreement is not ambiguity. Two versions declaring the SAME command
230    /// resolve without the run's identity, exactly as before.
231    #[test]
232    fn identical_bodies_across_versions_still_collapse_to_one() {
233        let retained = [
234            contract(0x0a, "sweep", false, "collect", Some("echo same")),
235            contract(0x0b, "sweep", true, "collect", Some("echo same")),
236        ];
237        let lookup = select_declared_body(&retained, "collect", None);
238        assert_eq!(command_of(&lookup), Some("echo same"), "{lookup:?}");
239    }
240
241    /// An action nothing declares a body for is delegated under either reading.
242    #[test]
243    fn a_bodiless_action_is_none_with_or_without_the_run_version() {
244        let retained = [contract(0x0a, "sweep", true, "collect", None)];
245        assert!(matches!(
246            select_declared_body(&retained, "collect", None),
247            DeclaredBodyLookup::None
248        ));
249        assert!(matches!(
250            select_declared_body(&retained, "collect", Some(&version(0x0a))),
251            DeclaredBodyLookup::None
252        ));
253    }
254
255    /// The refusal the blind reading produces still carries what an operator
256    /// needs — the fix narrows when it fires, it does not hollow it out.
257    #[test]
258    fn the_blind_refusal_still_names_every_declaring_version() -> Result<(), String> {
259        let retained = [
260            contract(0x0a, "sweep", false, "collect", Some("echo old")),
261            contract(0x0b, "sweep", true, "collect", Some("echo new")),
262        ];
263        let lookup = select_declared_body(&retained, "collect", None);
264        let DeclaredBodyLookup::Ambiguous { declaring } = lookup else {
265            return Err(format!(
266                "different bodies must refuse when read blind: {lookup:?}"
267            ));
268        };
269        assert_eq!(declaring.len(), 2, "{declaring:?}");
270        assert_eq!(declaring[0].content_hash, version(0x0a).to_string());
271        assert!(!declaring[0].route_active);
272        assert!(declaring[1].route_active);
273        assert_ne!(
274            declaring[0].body, declaring[1].body,
275            "the two versions must be recorded as carrying DIFFERENT bodies"
276        );
277        Ok(())
278    }
279}