Skip to main content

aion/loader/
load.rs

1//! Package staging: validated load units shared by the workflow catalog.
2
3use std::collections::HashSet;
4
5use aion_package::{ContentHash, ManifestDigest, ManifestVersion, Package};
6
7use crate::error::EngineError;
8
9/// Outcome of one package load, computed inside the catalog mutation lock.
10///
11/// `freshly_loaded` distinguishes a real registration from an idempotent
12/// re-load of a resident hash; `route_changed` reports whether the call
13/// re-pointed routing (false means the hash was already route-active and the
14/// load was a full no-op). Both flags are race-free truth captured under the
15/// same lock that committed the mutation, never a list-before/list-after read.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct LoadOutcome {
18    /// The loaded (or already-resident) workflow record.
19    pub record: LoadedWorkflow,
20    /// True when this call registered the version; false on idempotent re-load.
21    pub freshly_loaded: bool,
22    /// True when this call re-pointed the type's route at the version.
23    pub route_changed: bool,
24}
25
26/// Workflow package entrypoint registered in the embedded runtime.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct LoadedWorkflow {
29    workflow_type: String,
30    deployed_entry_module: String,
31    entry_function: String,
32    version: ContentHash,
33}
34
35impl LoadedWorkflow {
36    /// Assembles a loaded-workflow record from already-validated parts.
37    pub(crate) const fn from_parts(
38        workflow_type: String,
39        deployed_entry_module: String,
40        entry_function: String,
41        version: ContentHash,
42    ) -> Self {
43        Self {
44            workflow_type,
45            deployed_entry_module,
46            entry_function,
47            version,
48        }
49    }
50
51    /// Logical workflow type from the package manifest entry module.
52    #[must_use]
53    pub fn workflow_type(&self) -> &str {
54        &self.workflow_type
55    }
56
57    /// Namespaced module name to spawn for this package version.
58    #[must_use]
59    pub fn deployed_entry_module(&self) -> &str {
60        &self.deployed_entry_module
61    }
62
63    /// Exported function to spawn for this package version.
64    #[must_use]
65    pub fn entry_function(&self) -> &str {
66        &self.entry_function
67    }
68
69    /// Content-hash version identifying this package.
70    #[must_use]
71    pub fn version(&self) -> &ContentHash {
72        &self.version
73    }
74}
75
76/// One workflow entry staged from a package manifest.
77pub(crate) struct StagedWorkflow {
78    pub(crate) workflow_type: String,
79    pub(crate) deployed_entry_module: String,
80    pub(crate) entry_function: String,
81}
82
83/// One package validated and decomposed into deployable module units.
84pub(crate) struct StagedLoad<'a> {
85    pub(crate) workflows: Vec<StagedWorkflow>,
86    pub(crate) manifest_version: ManifestVersion,
87    pub(crate) manifest_digest: ManifestDigest,
88    pub(crate) version: ContentHash,
89    pub(crate) modules: Vec<StagedModule<'a>>,
90}
91
92impl<'a> StagedLoad<'a> {
93    pub(crate) fn new(package: &'a Package) -> Result<Self, EngineError> {
94        let manifest = package.manifest();
95        let version = package.content_hash().clone();
96        let mut seen = HashSet::new();
97        let mut workflows = Vec::with_capacity(1 + manifest.additional_workflows.len());
98        let entries = std::iter::once((
99            manifest.entry_module.as_str(),
100            manifest.entry_module.as_str(),
101            manifest.entry_function.as_str(),
102        ))
103        .chain(manifest.additional_workflows.iter().map(|entry| {
104            (
105                entry.workflow_type.as_str(),
106                entry.entry_module.as_str(),
107                entry.entry_function.as_str(),
108            )
109        }));
110        for (workflow_type, entry_module, entry_function) in entries {
111            if !seen.insert(workflow_type) {
112                return Err(load_error(format!(
113                    "package declares workflow type `{workflow_type}` more than once"
114                )));
115            }
116            if package.beams().get(entry_module).is_none() {
117                return Err(load_error(format!(
118                    "manifest entry module `{entry_module}` for workflow `{workflow_type}` is absent from package beams"
119                )));
120            }
121            workflows.push(StagedWorkflow {
122                workflow_type: workflow_type.to_owned(),
123                deployed_entry_module: aion_package::deployed_name(entry_module, &version),
124                entry_function: entry_function.to_owned(),
125            });
126        }
127        let modules = package
128            .deployed_modules()
129            .into_iter()
130            .map(|(deployed_name, bytes)| StagedModule {
131                deployed_name,
132                bytes,
133            })
134            .collect();
135
136        Ok(Self {
137            workflows,
138            manifest_version: manifest.version.clone(),
139            manifest_digest: manifest.canonical_digest()?,
140            version,
141            modules,
142        })
143    }
144
145    /// Loaded-workflow records this package commits atomically.
146    pub(crate) fn records(&self) -> Vec<LoadedWorkflow> {
147        self.workflows
148            .iter()
149            .map(|entry| {
150                LoadedWorkflow::from_parts(
151                    entry.workflow_type.clone(),
152                    entry.deployed_entry_module.clone(),
153                    entry.entry_function.clone(),
154                    self.version.clone(),
155                )
156            })
157            .collect()
158    }
159}
160
161/// One deployable module of a staged package.
162pub(crate) struct StagedModule<'a> {
163    pub(crate) deployed_name: String,
164    pub(crate) bytes: &'a [u8],
165}
166
167pub(crate) fn load_error(reason: String) -> EngineError {
168    EngineError::Load { reason }
169}
170
171/// Best-effort rollback of modules registered before a failed load step.
172///
173/// Returns a human-readable suffix describing rollback failures, empty when
174/// every registration was unwound cleanly.
175pub(crate) fn rollback_registered<R>(rollback: &mut R, registered_now: &[String]) -> String
176where
177    R: FnMut(&str) -> Result<(), EngineError>,
178{
179    let mut errors = Vec::new();
180    for deployed_name in registered_now.iter().rev() {
181        if let Err(error) = rollback(deployed_name) {
182            errors.push(format!("{deployed_name}: {error}"));
183        }
184    }
185
186    if errors.is_empty() {
187        String::new()
188    } else {
189        format!("; rollback failed for {}", errors.join(", "))
190    }
191}