Skip to main content

harmos_runtime/operations/
registration.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::sync::Arc;
18// Third Party Imports
19
20// Local Imports
21use super::manager::Entry;
22use super::{Binding, Implementations, WireImplementations};
23use super::{Context, OperationManager};
24use crate::catalog::{Catalog, Projection, Visitor};
25
26#[cfg(any(feature = "plugin-sidecar", all(feature = "plugin-guest", not(target_arch = "wasm32"))))]
27use crate::operations::Declarations;
28use crate::operations::{AuthoredDeclarations, Definition, OperationDeclaration, WireDeclaration};
29#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
30use crate::plugin::PluginScope;
31use crate::schema::Described;
32use crate::{Error, Message, Result, operations::Lane};
33
34/* === Constants === */
35
36/* === Macros === */
37
38/* === Type Aliases === */
39
40type BindCatalog = dyn Fn(&OperationManager) -> Result<()> + Send + Sync;
41
42/* === Structs === */
43
44pub(super) struct WireProjection {
45    declaration: std::any::TypeId,
46    contract: crate::operations::Contract,
47    dispatch: Option<super::wire::Dispatch>,
48
49    #[cfg(feature = "grpc")]
50    grpc: Option<crate::grpc::routing::Handler>,
51}
52
53#[derive(Clone)]
54pub(crate) struct CatalogBinding {
55    bind: Arc<BindCatalog>,
56}
57
58pub struct Bindings<'a> {
59    pub(super) operations: &'a OperationManager,
60    owner: &'a str,
61
62    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
63    pub(super) scope: Option<&'a PluginScope>,
64
65    pub(super) entries: Vec<Arc<Entry>>,
66}
67
68#[must_use = "retain plugin operation registration until shutdown settles its invocations"]
69#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
70pub struct PluginRegistration {
71    scope: Arc<PluginScope>,
72    operations: OperationManager,
73    entries: Vec<Arc<Entry>>,
74}
75
76/* === Enums === */
77
78/* === Traits === */
79
80/* === Implementations === */
81
82impl CatalogBinding {
83    pub(crate) fn operation<T>(owner: String, lane: impl Lane, resources: Arc<T::Resources>) -> Self
84    where
85        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
86    {
87        let lane = lane.name();
88        Self { bind: Arc::new(move |operations| operations.register_operation_at::<T>(owner.clone(), lane, resources.clone())) }
89    }
90
91    pub(crate) fn local<C, R>(owner: String, lane: impl Lane, resources: Arc<R>) -> Self
92    where
93        C: Catalog<AuthoredDeclarations> + Catalog<Implementations<R>>,
94        R: Send + Sync + 'static,
95    {
96        let lane = lane.name();
97        Self {
98            bind: Arc::new(move |operations| {
99                operations.register_with(owner.clone(), |bindings| {
100                    bindings.project_owned_at::<C, Implementations<R>, R>(lane, Arc::clone(&resources))
101                })
102            }),
103        }
104    }
105
106    pub(crate) fn wire<C, R>(owner: String, lane: impl Lane, resources: Arc<R>) -> Self
107    where
108        C: Catalog<AuthoredDeclarations> + Catalog<WireImplementations<R>>,
109        R: Send + Sync + 'static,
110    {
111        let lane = lane.name();
112        Self {
113            bind: Arc::new(move |operations| {
114                operations.register_with(owner.clone(), |bindings| {
115                    bindings.project_owned_at::<C, WireImplementations<R>, R>(lane, Arc::clone(&resources))
116                })
117            }),
118        }
119    }
120
121    pub(crate) fn bind(&self, operations: &OperationManager) -> Result<()> {
122        (self.bind)(operations)
123    }
124}
125
126impl OperationManager {
127    pub fn register_operations<C, R>(&self, owner: impl Into<String>, lane: impl Lane, resources: Arc<R>) -> Result<()>
128    where
129        R: Send + Sync + 'static,
130        C: Catalog<AuthoredDeclarations> + Catalog<Implementations<R>>,
131    {
132        self.register_with(owner, |bindings| bindings.register_operations::<C, R>(lane, resources))
133    }
134
135    pub fn register_wire_operations<C, R>(&self, owner: impl Into<String>, lane: impl Lane, resources: Arc<R>) -> Result<()>
136    where
137        R: Send + Sync + 'static,
138        C: Catalog<AuthoredDeclarations> + Catalog<WireImplementations<R>>,
139    {
140        self.register_with(owner, |bindings| bindings.register_wire_operations::<C, R>(lane, resources))
141    }
142
143    pub(super) fn register_with(&self, owner: impl Into<String>, configure: impl FnOnce(&mut Bindings<'_>) -> Result<()>) -> Result<()> {
144        let owner = owner.into();
145        let mut bindings = Bindings {
146            operations: self,
147            owner: &owner,
148            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
149            scope: None,
150            entries: Vec::new(),
151        };
152        configure(&mut bindings)?;
153        self.reserve(&bindings.entries)
154    }
155
156    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
157    pub fn stage_plugin(&self, scope: &PluginScope, configure: impl FnOnce(&mut Bindings<'_>) -> Result<()>) -> Result<PluginRegistration> {
158        if scope.is_closed() {
159            return Err(Error::Closing { domain: "plugin scope" });
160        }
161        if scope.has_committed() {
162            return Err(Error::InvalidInput { field: "operation catalog", reason: "plugin generation is already committed" });
163        }
164        let mut bindings = Bindings { operations: self, owner: scope.owner(), scope: Some(scope), entries: Vec::new() };
165        configure(&mut bindings)?;
166        scope.validate_operations(bindings.entries.iter().map(|entry| entry.declared()))?;
167        scope.validate_wire_contracts(bindings.entries.iter().filter_map(|entry| entry.contract.as_ref()))?;
168        self.reserve(&bindings.entries)?;
169        #[cfg(feature = "plugin-sidecar")]
170        for entry in &bindings.entries {
171            if let Some(native) = &entry.native
172                && let Err(error) = native.capture_observe_contract()
173            {
174                self.fence(&bindings.entries, true);
175                self.remove_settled(&bindings.entries);
176                return Err(error);
177            }
178        }
179        let receipt_scope = Arc::new(scope.clone());
180        let staged = self
181            .track_staging(&receipt_scope)
182            .and_then(|_| self.validate_staging(&bindings.entries))
183            .and_then(|_| scope.bind_operations())
184            .and_then(|_| self.validate_staging(&bindings.entries));
185        if let Err(error) = staged {
186            scope.close_staging();
187            self.fence(&bindings.entries, true);
188            self.remove_settled(&bindings.entries);
189            return Err(error);
190        }
191        Ok(PluginRegistration { scope: receipt_scope, operations: self.clone(), entries: bindings.entries })
192    }
193}
194
195impl Bindings<'_> {
196    pub fn register_operations<C, R>(&mut self, lane: impl Lane, resources: Arc<R>) -> Result<()>
197    where
198        R: Send + Sync + 'static,
199        C: Catalog<AuthoredDeclarations> + Catalog<Implementations<R>>,
200    {
201        self.project_owned_at::<C, Implementations<R>, R>(lane.name(), resources)
202    }
203
204    pub fn register_wire_operations<C, R>(&mut self, lane: impl Lane, resources: Arc<R>) -> Result<()>
205    where
206        R: Send + Sync + 'static,
207        C: Catalog<AuthoredDeclarations> + Catalog<WireImplementations<R>>,
208    {
209        self.project_owned_at::<C, WireImplementations<R>, R>(lane.name(), resources)
210    }
211
212    pub fn register_operation<T>(&mut self, lane: impl Lane, resources: Arc<T::Resources>) -> Result<()>
213    where
214        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
215    {
216        self.register_owned_at::<T>(lane.name(), resources)
217    }
218
219    pub(super) fn register_owned_at<T>(&mut self, lane: &'static str, resources: Arc<T::Resources>) -> Result<()>
220    where
221        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
222    {
223        self.register_operation_id_at::<T>(T::Declaration::ID, lane, resources)
224    }
225
226    pub fn register_operation_id<T>(&mut self, id: impl Into<String>, lane: impl Lane, resources: Arc<T::Resources>) -> Result<()>
227    where
228        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
229    {
230        self.register_operation_id_at::<T>(id, lane.name(), resources)
231    }
232
233    fn register_operation_id_at<T>(&mut self, id: impl Into<String>, lane: &'static str, resources: Arc<T::Resources>) -> Result<()>
234    where
235        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
236    {
237        let mut definition = Definition::of::<T::Declaration>().map_err(|source| Error::OperationDeclaration { source })?;
238        definition.id = crate::Identifier::new(id.into()).map_err(|source| Error::OperationDeclaration { source: source.into() })?;
239        let id = definition.id.as_str();
240        if self.entries.iter().any(|entry| entry.id() == id) {
241            return Err(Error::AlreadyRegistered { domain: "operation", name: id.to_owned() });
242        }
243        let mut projected = project::<T::Declaration>()?;
244        if projected.declaration != std::any::TypeId::of::<T::Declaration>() {
245            return Err(Error::InvalidInput {
246                field: "operation wire declaration",
247                reason: "wire projection must match the original typed declaration",
248            });
249        }
250        // An id override keeps the declared contract under the new identity; a gRPC method keeps its declared identity.
251        if projected.contract.operation() != &definition {
252            let original = &projected.contract;
253            if original.grpc().is_some() {
254                return Err(Error::InvalidInput {
255                    field: "operation wire declaration",
256                    reason: "an operation that binds a gRPC method keeps its declared identity",
257                });
258            }
259            projected.contract =
260                crate::operations::Contract::new(definition.clone(), original.version(), original.codec(), original.schema().clone())
261                    .map_err(|source| Error::OperationFailed { operation: definition.id.to_string(), source: Arc::new(source) })?;
262        }
263        let projected = Some(projected);
264        let mut entry = self.operations.binding_owned_definition::<T>(
265            self.owner,
266            lane,
267            resources,
268            definition,
269            projected.as_ref().and_then(|wire| wire.dispatch),
270            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
271            self.scope.cloned(),
272        )?;
273        if let Some(projected) = projected {
274            let original = Arc::get_mut(&mut entry).expect("new owned entry has one owner");
275            original.contract = Some(projected.contract);
276            #[cfg(feature = "grpc")]
277            {
278                original.grpc = projected.grpc;
279            }
280        }
281        self.entries.push(entry);
282        Ok(())
283    }
284
285    pub fn register_wire_operation<T>(&mut self, lane: impl Lane, resources: Arc<T::Resources>) -> Result<()>
286    where
287        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
288        T::Declaration: crate::operations::OperationDeclaration,
289        T::Declaration: Message + Described,
290    {
291        self.register_owned_wire_at::<T>(lane.name(), resources)
292    }
293
294    pub(super) fn register_owned_wire_at<T>(&mut self, lane: &'static str, resources: Arc<T::Resources>) -> Result<()>
295    where
296        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
297        T::Declaration: crate::operations::OperationDeclaration,
298        T::Declaration: Message + Described,
299    {
300        let contract = WireDeclaration::of::<T::Declaration>()
301            .contract()
302            .map_err(|source| Error::OperationFailed { operation: T::Declaration::ID.to_owned(), source: Arc::new(source) })?;
303        self.register_wire_contract_at::<T>(&contract, lane, resources)
304    }
305
306    pub fn register_wire_contract<T>(
307        &mut self, contract: &crate::operations::Contract, lane: impl Lane, resources: Arc<T::Resources>,
308    ) -> Result<()>
309    where
310        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
311        T::Declaration: crate::operations::OperationDeclaration,
312        T::Declaration: Message + Described,
313    {
314        self.register_wire_contract_at::<T>(contract, lane.name(), resources)
315    }
316
317    fn register_wire_contract_at<T>(
318        &mut self, contract: &crate::operations::Contract, lane: &'static str, resources: Arc<T::Resources>,
319    ) -> Result<()>
320    where
321        T: for<'ctx> super::Operation<Context<'ctx> = Context<'ctx>>,
322        T::Declaration: crate::operations::OperationDeclaration,
323        T::Declaration: Message + Described,
324    {
325        let projected = project::<T::Declaration>()?;
326        let declared = &projected.contract;
327        let mut definition = declared.operation().clone();
328        definition.id = contract.operation().id.clone();
329        if definition != *contract.operation()
330            || declared.version() != contract.version()
331            || declared.codec() != contract.codec()
332            || ![
333                (Some(declared.request()), Some(contract.request())),
334                (Some(declared.response()), Some(contract.response())),
335                (declared.input(), contract.input()),
336                (declared.output(), contract.output()),
337            ]
338            .into_iter()
339            .all(|(left, right)| match (left, right) {
340                (None, None) => true,
341                (Some(left), Some(right)) => left
342                    .schema()
343                    .equivalent(left.index(), right.schema(), right.index(), &harmos_domain::config::SchemaSettings::default())
344                    .unwrap_or(false),
345                _ => false,
346            })
347            || declared.grpc() != contract.grpc()
348        {
349            return Err(Error::InvalidInput { field: "operation contract", reason: "runtime binding may override identity only" });
350        }
351        let id = contract.operation().id.as_str();
352        if self.entries.iter().any(|entry| entry.id() == id) {
353            return Err(Error::AlreadyRegistered { domain: "operation", name: id.to_owned() });
354        }
355        let mut entry = self.operations.binding_owned_definition::<T>(
356            self.owner,
357            lane,
358            resources,
359            contract.operation().clone(),
360            projected.dispatch,
361            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
362            self.scope.cloned(),
363        )?;
364        let original = Arc::get_mut(&mut entry).expect("new owned entry has one owner");
365        original.contract = Some(contract.clone());
366        #[cfg(feature = "grpc")]
367        {
368            original.grpc = projected.grpc;
369        }
370        self.entries.push(entry);
371        Ok(())
372    }
373
374    #[cfg(any(feature = "plugin-sidecar", all(feature = "plugin-guest", not(target_arch = "wasm32"))))]
375    pub(super) fn project<C, V, H>(&mut self, lane: impl Lane, handler: Arc<H>) -> Result<()>
376    where
377        C: Catalog<Declarations> + Catalog<V>,
378        V: Visitor<Entry = Binding<H>>,
379        H: Send + Sync + 'static,
380    {
381        self.project_at::<C, V, H>(lane.name(), handler)
382    }
383
384    #[cfg(any(feature = "plugin-sidecar", all(feature = "plugin-guest", not(target_arch = "wasm32"))))]
385    fn project_at<C, V, H>(&mut self, lane: &'static str, handler: Arc<H>) -> Result<()>
386    where
387        C: Catalog<Declarations> + Catalog<V>,
388        V: Visitor<Entry = Binding<H>>,
389        H: Send + Sync + 'static,
390    {
391        let definitions = Declarations::of::<C>().map_err(|source| Error::OperationDeclaration { source })?;
392        self.apply_projection(lane, handler, &definitions, &<C as Catalog<V>>::ENTRIES)
393    }
394
395    fn project_owned_at<C, V, R>(&mut self, lane: &'static str, resources: Arc<R>) -> Result<()>
396    where
397        C: Catalog<AuthoredDeclarations> + Catalog<V>,
398        V: Visitor<Entry = Binding<R>>,
399        R: Send + Sync + 'static,
400    {
401        let definitions = AuthoredDeclarations::of::<C>().map_err(|source| Error::OperationDeclaration { source })?;
402        self.apply_projection(lane, resources, &definitions, &<C as Catalog<V>>::ENTRIES)
403    }
404
405    fn apply_projection<H: Send + Sync + 'static>(
406        &mut self, lane: &'static str, handler: Arc<H>, definitions: &[Definition], entries: &impl Projection<Binding<H>>,
407    ) -> Result<()> {
408        let previous = self.entries.len();
409        if let Err(error) = entries.try_for_each(|entry| entry.apply(self, lane, Arc::clone(&handler))) {
410            self.entries.truncate(previous);
411            return Err(error);
412        }
413        let projected = &self.entries[previous..];
414        if projected.len() != definitions.len() || projected.iter().zip(definitions).any(|(entry, declared)| entry.declared() != declared) {
415            self.entries.truncate(previous);
416            return Err(Error::InvalidInput { field: "operation catalog", reason: "metadata and handler projections differ" });
417        }
418        Ok(())
419    }
420}
421
422#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
423impl PluginRegistration {
424    pub async fn shutdown(&mut self) -> Result<()> {
425        self.scope.unbind_operations();
426        self.operations.fence(&self.entries, false);
427        self.operations.settle_entries(&self.entries).await?;
428        self.entries.clear();
429        Ok(())
430    }
431}
432
433#[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
434impl Drop for PluginRegistration {
435    fn drop(&mut self) {
436        self.scope.close_staging();
437        self.scope.unbind_operations();
438        self.operations.fence(&self.entries, true);
439        self.operations.remove_settled(&self.entries);
440        // Running entries remain fenced and reserved in the manager. A later
441        // reservation prunes settled tombstones; runtime shutdown drains all.
442    }
443}
444
445/* === Public Functions === */
446
447// The manager's one projection of a declaration: its wire contract, its
448// unary dispatch and, when it binds a method, its gRPC handler.
449pub(super) fn project<O: crate::operations::OperationDeclaration>() -> Result<WireProjection> {
450    let contract = WireDeclaration::of::<O>()
451        .contract()
452        .map_err(|source| Error::OperationFailed { operation: O::ID.to_owned(), source: Arc::new(source) })?;
453    let streaming = contract.input().is_some() || contract.output().is_some();
454    #[cfg(feature = "grpc")]
455    let grpc = contract.grpc().map(|_| crate::grpc::routing::handler::<O>());
456    Ok(WireProjection {
457        declaration: std::any::TypeId::of::<O>(),
458        contract,
459        dispatch: if streaming { None } else { Some(super::wire::dispatch::<O>) },
460        #[cfg(feature = "grpc")]
461        grpc,
462    })
463}
464
465/* === Private Functions === */
466
467/* === Tests === */