Skip to main content

lenso_kernel/
request.rs

1use std::time::Duration;
2
3use super::{
4    CancellationToken, EventCapability, InvocationContext, LocalBoxFuture,
5    ModuleEventDependencyHandle, NativeAppRuntime, NativeEndpointBinding, NativeEventHandle,
6    NativeRequestEndpoint, NativeRequestHandle, NativeStreamEndpointBinding, NativeStreamHandle,
7    Rc, RefCell, StreamCapability, Weak,
8};
9
10pub trait RequestCapability: 'static {
11    /// Typed request value.
12    type Request: 'static;
13    /// Typed success value.
14    type Response: 'static;
15    /// Typed Capability-defined error value.
16    type DomainError: 'static;
17    /// Stable Capability series identity.
18    const ID: &'static str;
19    /// Exact generated Descriptor version.
20    const DESCRIPTOR_VERSION: &'static str;
21
22    /// Invokes one native endpoint using the most specific binding available.
23    ///
24    /// Generated bindings override this hook with a typed path. Older bindings retain the
25    /// type-erased compatibility path without requiring regeneration.
26    #[doc(hidden)]
27    fn invoke_native(
28        endpoint: &dyn NativeRequestEndpoint,
29        operation: &str,
30        request: Self::Request,
31        context: InvocationContext,
32    ) -> NativeRequestFuture<Self>
33    where
34        Self: Sized,
35    {
36        invoke_typed_or_erased_native_request::<Self>(endpoint, operation, request, context)
37    }
38}
39
40/// A typed native request result before Kernel cancellation and supervision are applied.
41#[doc(hidden)]
42pub type NativeRequestFuture<C> = LocalBoxFuture<
43    'static,
44    Result<
45        Result<<C as RequestCapability>::Response, <C as RequestCapability>::DomainError>,
46        RuntimeFailure,
47    >,
48>;
49
50type TypedNativeRequestFn<C> =
51    dyn Fn(&str, <C as RequestCapability>::Request, InvocationContext) -> NativeRequestFuture<C>;
52
53/// Runtime-provided typed endpoint used when a request crosses an execution boundary.
54///
55/// Generated in-process endpoints may expose a more specific endpoint type. This generic
56/// carrier lets execution adapters preserve typed request, response, and domain-error values
57/// without routing them through `Box<dyn Any>`.
58#[doc(hidden)]
59pub struct TypedNativeRequestEndpoint<C: RequestCapability> {
60    invoke: Rc<TypedNativeRequestFn<C>>,
61}
62
63impl<C: RequestCapability> TypedNativeRequestEndpoint<C> {
64    /// Creates a typed endpoint around one runtime-owned dispatcher.
65    pub fn new(
66        invoke: impl Fn(&str, C::Request, InvocationContext) -> NativeRequestFuture<C> + 'static,
67    ) -> Self {
68        Self {
69            invoke: Rc::new(invoke),
70        }
71    }
72
73    /// Dispatches one request without type erasure.
74    pub fn invoke(
75        &self,
76        operation: &str,
77        request: C::Request,
78        context: InvocationContext,
79    ) -> NativeRequestFuture<C> {
80        (self.invoke)(operation, request, context)
81    }
82}
83
84impl<C: RequestCapability> std::fmt::Debug for TypedNativeRequestEndpoint<C> {
85    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        formatter
87            .debug_struct("TypedNativeRequestEndpoint")
88            .field("capability", &C::ID)
89            .finish_non_exhaustive()
90    }
91}
92
93/// Uses a runtime-provided typed endpoint when present, retaining erased compatibility.
94#[doc(hidden)]
95pub fn invoke_typed_or_erased_native_request<C: RequestCapability>(
96    endpoint: &dyn NativeRequestEndpoint,
97    operation: &str,
98    request: C::Request,
99    context: InvocationContext,
100) -> NativeRequestFuture<C> {
101    if let Some(endpoint) = endpoint
102        .typed_endpoint()
103        .and_then(|endpoint| endpoint.downcast_ref::<TypedNativeRequestEndpoint<C>>())
104    {
105        endpoint.invoke(operation, request, context)
106    } else {
107        invoke_erased_native_request::<C>(endpoint, operation, request, context)
108    }
109}
110
111/// Compatibility dispatcher used by generated bindings when a typed endpoint is unavailable.
112#[doc(hidden)]
113pub fn invoke_erased_native_request<C: RequestCapability>(
114    endpoint: &dyn NativeRequestEndpoint,
115    operation: &str,
116    request: C::Request,
117    context: InvocationContext,
118) -> NativeRequestFuture<C> {
119    let invocation = endpoint.invoke(operation, Box::new(request), context);
120    Box::pin(async move {
121        match invocation.await? {
122            Ok(value) => value
123                .downcast::<C::Response>()
124                .map(|value| Ok(*value))
125                .map_err(|_| RuntimeFailure::ProtocolViolation { capability: C::ID }),
126            Err(value) => value
127                .downcast::<C::DomainError>()
128                .map(|value| Err(*value))
129                .map_err(|_| RuntimeFailure::ProtocolViolation { capability: C::ID }),
130        }
131    })
132}
133
134/// Kernel-generated identity for one logical request invocation.
135pub type RequestId = u64;
136
137/// Runtime-owned failure, kept separate from Capability-defined Domain Errors.
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub enum RuntimeFailure {
140    /// The consumer has no resolved binding for this Capability.
141    Unavailable { capability: &'static str },
142    /// The bound provider does not declare the requested Operation.
143    UnknownOperation {
144        capability: &'static str,
145        operation: String,
146    },
147    /// A singular generated client was used for a requirement with many providers.
148    AmbiguousBinding {
149        capability: &'static str,
150        providers: usize,
151    },
152    /// Generated native types disagreed with the prepared endpoint.
153    ProtocolViolation { capability: &'static str },
154    /// A package selected by the Plan was not linked into the native App.
155    MissingModuleFactory {
156        instance: String,
157        package_id: String,
158    },
159    /// No installed Execution Adapter provides the class selected by one Instance.
160    UnavailableExecutionClass {
161        instance_key: String,
162        execution_class: String,
163    },
164    /// The Resolved Plan or prepared endpoint set is internally inconsistent.
165    InvalidResolvedPlan { detail: String },
166    /// New request admission was closed because the App is shutting down.
167    AdmissionClosed,
168    /// The request could not enter a full bounded admission queue.
169    ResourceExhausted {
170        capability: &'static str,
171        operation: String,
172    },
173    /// The invocation deadline expired before the request completed.
174    DeadlineExceeded { request_id: RequestId },
175    /// The caller cancelled the invocation before it completed.
176    Cancelled { request_id: RequestId },
177    /// The Runtime Driver or Adapter reported an internal execution failure.
178    Internal { detail: String },
179    /// A Module generation reported a failure that should trigger supervision.
180    ModuleFailure { detail: String },
181    /// A Module Instance exhausted its finite restart budget.
182    ModuleRestartExhausted { instance: String, attempts: usize },
183}
184
185/// The lifecycle phase represented by a Module context.
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum ModuleLifecyclePhase {
188    /// The Module may validate configuration and reserve reversible resources.
189    Prepare,
190    /// The Module may initialize against already prepared dependencies.
191    Activate,
192    /// The App Ready Gate has opened and externally triggered work may begin.
193    Ready,
194    /// The Module must release work and resources owned by this generation.
195    Deactivate,
196}
197
198#[cfg(test)]
199mod typed_endpoint_tests {
200    use std::any::Any;
201
202    use super::*;
203
204    #[derive(Debug)]
205    struct Echo;
206
207    impl RequestCapability for Echo {
208        type Request = u64;
209        type Response = u64;
210        type DomainError = ();
211        const ID: &'static str = "test.echo@1";
212        const DESCRIPTOR_VERSION: &'static str = "1.0.0";
213    }
214
215    #[derive(Debug)]
216    struct Endpoint {
217        typed: TypedNativeRequestEndpoint<Echo>,
218    }
219
220    impl NativeRequestEndpoint for Endpoint {
221        fn capability_id(&self) -> &'static str {
222            Echo::ID
223        }
224
225        fn descriptor_version(&self) -> &'static str {
226            Echo::DESCRIPTOR_VERSION
227        }
228
229        fn operations(&self) -> &'static [&'static str] {
230            &["echo"]
231        }
232
233        fn typed_endpoint(&self) -> Option<&dyn Any> {
234            Some(&self.typed)
235        }
236
237        fn invoke(
238            &self,
239            _operation: &str,
240            _request: Box<dyn Any>,
241            _context: InvocationContext,
242        ) -> LocalBoxFuture<'static, Result<crate::ErasedDomainResult, RuntimeFailure>> {
243            panic!("typed dispatch must not call the erased endpoint")
244        }
245    }
246
247    #[test]
248    fn default_dispatch_uses_runtime_typed_endpoint() {
249        let endpoint = Endpoint {
250            typed: TypedNativeRequestEndpoint::new(|_, request, _| {
251                Box::pin(futures::future::ready(Ok(Ok(request + 1))))
252            }),
253        };
254        let context = InvocationContext::new(1, None, CancellationToken::new());
255
256        let result =
257            futures::executor::block_on(Echo::invoke_native(&endpoint, "echo", 41, context));
258
259        assert_eq!(result, Ok(Ok(42)));
260    }
261}
262
263/// A deterministic dependency visible to one Module Instance.
264#[derive(Clone, Debug)]
265pub struct ModuleDependency {
266    pub(super) capability_id: String,
267    pub(super) provider_instance: String,
268    pub(super) provider_order: usize,
269    pub(super) handle: Option<ModuleDependencyHandle>,
270    pub(super) stream_handle: Option<ModuleStreamDependencyHandle>,
271    pub(super) event_handle: Option<ModuleEventDependencyHandle>,
272}
273
274impl ModuleDependency {
275    pub(super) fn new(
276        capability_id: impl Into<String>,
277        provider_instance: impl Into<String>,
278        provider_order: usize,
279        handle: Option<ModuleDependencyHandle>,
280        stream_handle: Option<ModuleStreamDependencyHandle>,
281        event_handle: Option<ModuleEventDependencyHandle>,
282    ) -> Self {
283        Self {
284            capability_id: capability_id.into(),
285            provider_instance: provider_instance.into(),
286            provider_order,
287            handle,
288            stream_handle,
289            event_handle,
290        }
291    }
292
293    /// Returns the Capability required by this dependency.
294    pub fn capability_id(&self) -> &str {
295        &self.capability_id
296    }
297
298    /// Returns the App-local provider Instance key.
299    pub fn provider_instance(&self) -> &str {
300        &self.provider_instance
301    }
302
303    /// Returns the deterministic provider order for a `many` binding.
304    pub const fn provider_order(&self) -> usize {
305        self.provider_order
306    }
307
308    /// Returns the resolved native endpoint handle when the Adapter supplied one.
309    pub fn handle(&self) -> Option<ModuleDependencyHandle> {
310        self.handle.clone()
311    }
312
313    /// Returns the resolved native stream endpoint handle when the Adapter supplied one.
314    pub fn stream_handle(&self) -> Option<ModuleStreamDependencyHandle> {
315        self.stream_handle.clone()
316    }
317
318    /// Returns the resolved native Event endpoint handle when the Adapter supplied one.
319    pub fn event_handle(&self) -> Option<ModuleEventDependencyHandle> {
320        self.event_handle.clone()
321    }
322}
323
324/// An opaque, Adapter-resolved Capability endpoint passed to lifecycle code.
325#[derive(Clone, Debug)]
326pub struct ModuleDependencyHandle {
327    pub(super) binding: NativeEndpointBinding,
328    pub(super) caller_instance: String,
329    pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
330}
331
332/// An opaque, Adapter-resolved stream Capability endpoint passed to lifecycle code.
333#[derive(Clone, Debug)]
334pub struct ModuleStreamDependencyHandle {
335    pub(super) binding: NativeStreamEndpointBinding,
336    pub(super) caller_instance: String,
337    pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
338}
339
340impl ModuleStreamDependencyHandle {
341    /// Returns the Capability implemented by this stream handle.
342    pub fn capability_id(&self) -> &'static str {
343        self.binding.state.capability_id
344    }
345
346    /// Returns the exact Descriptor version implemented by this stream handle.
347    pub fn descriptor_version(&self) -> &'static str {
348        self.binding.state.descriptor_version
349    }
350
351    /// Returns the exact stream Operation table implemented by this handle.
352    pub fn operations(&self) -> &'static [&'static str] {
353        self.binding.state.operations
354    }
355
356    /// Converts this resolved dependency into its generated typed stream handle.
357    pub fn typed<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
358        if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
359            return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
360        }
361        let runtime = self
362            .runtime
363            .borrow()
364            .upgrade()
365            .ok_or(RuntimeFailure::AdmissionClosed)?;
366        Ok(NativeStreamHandle::from_endpoints(
367            std::slice::from_ref(&self.binding),
368            runtime,
369            &self.caller_instance,
370            true,
371        ))
372    }
373}
374
375impl ModuleDependencyHandle {
376    /// Returns the Capability implemented by this handle.
377    pub fn capability_id(&self) -> &'static str {
378        self.binding.state.capability_id
379    }
380
381    /// Returns the exact Descriptor version implemented by this handle.
382    pub fn descriptor_version(&self) -> &'static str {
383        self.binding.state.descriptor_version
384    }
385
386    /// Returns the exact Operation table implemented by this handle.
387    pub fn operations(&self) -> &'static [&'static str] {
388        self.binding.state.operations
389    }
390
391    /// Converts this resolved dependency into its generated typed request handle.
392    pub fn typed<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
393        if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
394            return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
395        }
396        let runtime = self
397            .runtime
398            .borrow()
399            .upgrade()
400            .ok_or(RuntimeFailure::AdmissionClosed)?;
401        Ok(NativeRequestHandle::from_endpoints(
402            std::slice::from_ref(&self.binding),
403            runtime,
404            &self.caller_instance,
405            true,
406        ))
407    }
408}
409
410/// The explicit Capability dependencies available during Module lifecycle.
411#[derive(Clone, Debug, Default)]
412pub struct ModuleDependencies {
413    pub(super) bindings: Vec<ModuleDependency>,
414    pub(super) caller_instance: Rc<str>,
415    pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
416}
417
418impl ModuleDependencies {
419    pub(super) fn new(
420        caller_instance: impl Into<String>,
421        runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
422    ) -> Self {
423        Self {
424            bindings: Vec::new(),
425            caller_instance: Rc::from(caller_instance.into()),
426            runtime,
427        }
428    }
429
430    /// Returns dependencies in the order materialized by the Resolved App Plan.
431    pub fn bindings(&self) -> &[ModuleDependency] {
432        &self.bindings
433    }
434
435    /// Returns the number of explicit dependencies.
436    pub fn len(&self) -> usize {
437        self.bindings.len()
438    }
439
440    /// Returns whether this Module has no explicit dependencies.
441    pub fn is_empty(&self) -> bool {
442        self.bindings.is_empty()
443    }
444
445    /// Creates a Kernel Invocation Context for work initiated by this Module.
446    ///
447    /// The request identity and monotonic deadline come from the same Runtime
448    /// Driver as the App. The context is still owned by the caller and its
449    /// cancellation token remains explicit.
450    pub fn invocation_context(
451        &self,
452        deadline: Option<Duration>,
453        cancellation: CancellationToken,
454    ) -> Result<InvocationContext, RuntimeFailure> {
455        let runtime = self
456            .runtime
457            .borrow()
458            .upgrade()
459            .ok_or(RuntimeFailure::AdmissionClosed)?;
460        let request_id = runtime.request_ids.get();
461        runtime.request_ids.set(request_id.saturating_add(1));
462        Ok(InvocationContext::new(request_id, deadline, cancellation)
463            .with_shared_caller_instance(self.caller_instance.clone()))
464    }
465
466    /// Creates a Module Invocation Context with a Driver-relative deadline.
467    pub fn invocation_context_after(
468        &self,
469        timeout: Duration,
470        cancellation: CancellationToken,
471    ) -> Result<InvocationContext, RuntimeFailure> {
472        let runtime = self
473            .runtime
474            .borrow()
475            .upgrade()
476            .ok_or(RuntimeFailure::AdmissionClosed)?;
477        let deadline = (runtime.driver.now)().saturating_add(timeout);
478        drop(runtime);
479        self.invocation_context(Some(deadline), cancellation)
480    }
481
482    /// Returns the one explicitly bound typed dependency.
483    pub fn one<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
484        let handles: Vec<_> = self
485            .bindings
486            .iter()
487            .filter(|binding| binding.capability_id() == C::ID)
488            .filter_map(ModuleDependency::handle)
489            .collect();
490        match handles.as_slice() {
491            [handle] => handle.typed::<C>(),
492            [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
493            handles => Err(RuntimeFailure::AmbiguousBinding {
494                capability: C::ID,
495                providers: handles.len(),
496            }),
497        }
498    }
499
500    /// Returns an optional explicitly bound typed dependency.
501    pub fn optional<C: RequestCapability>(
502        &self,
503    ) -> Result<Option<NativeRequestHandle<C>>, RuntimeFailure> {
504        match self
505            .bindings
506            .iter()
507            .filter(|binding| binding.capability_id() == C::ID)
508            .filter_map(ModuleDependency::handle)
509            .collect::<Vec<_>>()
510            .as_slice()
511        {
512            [] => Ok(None),
513            [handle] => handle.typed::<C>().map(Some),
514            handles => Err(RuntimeFailure::AmbiguousBinding {
515                capability: C::ID,
516                providers: handles.len(),
517            }),
518        }
519    }
520
521    /// Returns all explicitly bound typed dependencies in resolved provider order.
522    pub fn many<C: RequestCapability>(
523        &self,
524    ) -> Result<Vec<NativeRequestHandle<C>>, RuntimeFailure> {
525        self.bindings
526            .iter()
527            .filter(|binding| binding.capability_id() == C::ID)
528            .filter_map(ModuleDependency::handle)
529            .map(|handle| handle.typed::<C>())
530            .collect()
531    }
532
533    /// Returns the one explicitly bound typed stream dependency.
534    pub fn one_stream<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
535        let handles: Vec<_> = self
536            .bindings
537            .iter()
538            .filter(|binding| binding.capability_id() == C::ID)
539            .filter_map(ModuleDependency::stream_handle)
540            .collect();
541        match handles.as_slice() {
542            [handle] => handle.typed::<C>(),
543            [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
544            handles => Err(RuntimeFailure::AmbiguousBinding {
545                capability: C::ID,
546                providers: handles.len(),
547            }),
548        }
549    }
550
551    /// Returns an optional explicitly bound typed stream dependency.
552    pub fn optional_stream<C: StreamCapability>(
553        &self,
554    ) -> Result<Option<NativeStreamHandle<C>>, RuntimeFailure> {
555        match self
556            .bindings
557            .iter()
558            .filter(|binding| binding.capability_id() == C::ID)
559            .filter_map(ModuleDependency::stream_handle)
560            .collect::<Vec<_>>()
561            .as_slice()
562        {
563            [] => Ok(None),
564            [handle] => handle.typed::<C>().map(Some),
565            handles => Err(RuntimeFailure::AmbiguousBinding {
566                capability: C::ID,
567                providers: handles.len(),
568            }),
569        }
570    }
571
572    /// Returns all explicitly bound typed stream dependencies in Plan order.
573    pub fn many_stream<C: StreamCapability>(
574        &self,
575    ) -> Result<Vec<NativeStreamHandle<C>>, RuntimeFailure> {
576        self.bindings
577            .iter()
578            .filter(|binding| binding.capability_id() == C::ID)
579            .filter_map(ModuleDependency::stream_handle)
580            .map(|handle| handle.typed::<C>())
581            .collect()
582    }
583
584    /// Returns one typed Event handle over every explicit binding in Plan order.
585    pub fn many_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
586        let handles: Vec<_> = self
587            .bindings
588            .iter()
589            .filter(|binding| binding.capability_id() == C::ID)
590            .filter_map(ModuleDependency::event_handle)
591            .collect();
592        if handles.iter().any(|handle| {
593            handle.capability_id() != C::ID || handle.descriptor_version() != C::DESCRIPTOR_VERSION
594        }) {
595            return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
596        }
597        let runtime = self
598            .runtime
599            .borrow()
600            .upgrade()
601            .ok_or(RuntimeFailure::AdmissionClosed)?;
602        let endpoints = handles
603            .iter()
604            .map(|handle| handle.binding.clone())
605            .collect::<Vec<_>>();
606        Ok(NativeEventHandle::from_endpoints(
607            &endpoints,
608            runtime,
609            &self.caller_instance,
610            true,
611        ))
612    }
613
614    /// Returns the one explicitly bound typed Event dependency.
615    pub fn one_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
616        match self
617            .bindings
618            .iter()
619            .filter(|binding| binding.capability_id() == C::ID)
620            .filter_map(ModuleDependency::event_handle)
621            .collect::<Vec<_>>()
622            .as_slice()
623        {
624            [handle] => handle.typed::<C>(),
625            [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
626            handles => Err(RuntimeFailure::AmbiguousBinding {
627                capability: C::ID,
628                providers: handles.len(),
629            }),
630        }
631    }
632
633    /// Returns an optional explicitly bound typed Event dependency.
634    pub fn optional_event<C: EventCapability>(
635        &self,
636    ) -> Result<Option<NativeEventHandle<C>>, RuntimeFailure> {
637        match self
638            .bindings
639            .iter()
640            .filter(|binding| binding.capability_id() == C::ID)
641            .filter_map(ModuleDependency::event_handle)
642            .collect::<Vec<_>>()
643            .as_slice()
644        {
645            [] => Ok(None),
646            [handle] => handle.typed::<C>().map(Some),
647            handles => Err(RuntimeFailure::AmbiguousBinding {
648                capability: C::ID,
649                providers: handles.len(),
650            }),
651        }
652    }
653}