1use std::{any::Any, time::Duration};
2
3use super::{
4 CancellationToken, EventCapability, InvocationContext, LocalBoxFuture, NativeAppRuntime,
5 NativeEndpointBinding, NativeEventHandle, NativeRequestEndpoint, NativeRequestHandle,
6 NativeStreamEndpointBinding, NativeStreamHandle, PluginEventDependencyHandle, Rc, RefCell,
7 StreamCapability, Weak,
8};
9
10pub trait RequestCapability: 'static {
11 type Request: 'static;
13 type Response: 'static;
15 type DomainError: 'static;
17 const ID: &'static str;
19 const DESCRIPTOR_VERSION: &'static str;
21
22 #[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#[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#[doc(hidden)]
59pub struct TypedNativeRequestEndpoint<C: RequestCapability> {
60 invoke: Rc<TypedNativeRequestFn<C>>,
61}
62
63impl<C: RequestCapability> TypedNativeRequestEndpoint<C> {
64 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 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#[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#[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
134pub type RequestId = u64;
136
137#[derive(Clone, Debug, Eq, PartialEq)]
139pub enum RuntimeFailure {
140 Unavailable { capability: &'static str },
142 UnknownOperation {
144 capability: &'static str,
145 operation: String,
146 },
147 AmbiguousBinding {
149 capability: &'static str,
150 providers: usize,
151 },
152 ProtocolViolation { capability: &'static str },
154 MissingPluginFactory {
156 instance: String,
157 package_id: String,
158 },
159 UnavailableExecutionClass {
161 instance_key: String,
162 execution_class: String,
163 },
164 InvalidResolvedPlan { detail: String },
166 AdmissionClosed,
168 ResourceExhausted {
170 capability: &'static str,
171 operation: String,
172 },
173 DeadlineExceeded { request_id: RequestId },
175 Cancelled { request_id: RequestId },
177 Internal { detail: String },
179 PluginFailure { detail: String },
181 PluginRestartExhausted { instance: String, attempts: usize },
183}
184
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum PluginLifecyclePhase {
188 Prepare,
190 Construct,
192 Activate,
194 Ready,
196 Deactivate,
198}
199
200#[cfg(test)]
201mod typed_endpoint_tests {
202 use std::any::Any;
203
204 use super::*;
205
206 #[derive(Debug)]
207 struct Echo;
208
209 impl RequestCapability for Echo {
210 type Request = u64;
211 type Response = u64;
212 type DomainError = ();
213 const ID: &'static str = "test.echo@1";
214 const DESCRIPTOR_VERSION: &'static str = "1.0.0";
215 }
216
217 #[derive(Debug)]
218 struct Endpoint {
219 typed: TypedNativeRequestEndpoint<Echo>,
220 }
221
222 impl NativeRequestEndpoint for Endpoint {
223 fn capability_id(&self) -> &'static str {
224 Echo::ID
225 }
226
227 fn descriptor_version(&self) -> &'static str {
228 Echo::DESCRIPTOR_VERSION
229 }
230
231 fn operations(&self) -> &'static [&'static str] {
232 &["echo"]
233 }
234
235 fn typed_endpoint(&self) -> Option<&dyn Any> {
236 Some(&self.typed)
237 }
238
239 fn invoke(
240 &self,
241 _operation: &str,
242 _request: Box<dyn Any>,
243 _context: InvocationContext,
244 ) -> LocalBoxFuture<'static, Result<crate::ErasedDomainResult, RuntimeFailure>> {
245 panic!("typed dispatch must not call the erased endpoint")
246 }
247 }
248
249 #[test]
250 fn default_dispatch_uses_runtime_typed_endpoint() {
251 let endpoint = Endpoint {
252 typed: TypedNativeRequestEndpoint::new(|_, request, _| {
253 Box::pin(futures::future::ready(Ok(Ok(request + 1))))
254 }),
255 };
256 let context = InvocationContext::new(1, None, CancellationToken::new());
257
258 let result =
259 futures::executor::block_on(Echo::invoke_native(&endpoint, "echo", 41, context));
260
261 assert_eq!(result, Ok(Ok(42)));
262 }
263}
264
265#[derive(Clone, Debug)]
267pub struct PluginDependency {
268 pub(super) requirement_id: String,
269 pub(super) capability_id: String,
270 pub(super) provider_instance: String,
271 pub(super) provider_order: usize,
272 pub(super) handle: Option<PluginDependencyHandle>,
273 pub(super) stream_handle: Option<PluginStreamDependencyHandle>,
274 pub(super) event_handle: Option<PluginEventDependencyHandle>,
275}
276
277impl PluginDependency {
278 pub(super) fn new(
279 requirement_id: impl Into<String>,
280 capability_id: impl Into<String>,
281 provider_instance: impl Into<String>,
282 provider_order: usize,
283 handle: Option<PluginDependencyHandle>,
284 stream_handle: Option<PluginStreamDependencyHandle>,
285 event_handle: Option<PluginEventDependencyHandle>,
286 ) -> Self {
287 Self {
288 requirement_id: requirement_id.into(),
289 capability_id: capability_id.into(),
290 provider_instance: provider_instance.into(),
291 provider_order,
292 handle,
293 stream_handle,
294 event_handle,
295 }
296 }
297
298 pub fn requirement_id(&self) -> &str {
300 &self.requirement_id
301 }
302
303 pub fn capability_id(&self) -> &str {
305 &self.capability_id
306 }
307
308 pub fn provider_instance(&self) -> &str {
310 &self.provider_instance
311 }
312
313 pub const fn provider_order(&self) -> usize {
315 self.provider_order
316 }
317
318 pub fn handle(&self) -> Option<PluginDependencyHandle> {
320 self.handle.clone()
321 }
322
323 pub fn stream_handle(&self) -> Option<PluginStreamDependencyHandle> {
325 self.stream_handle.clone()
326 }
327
328 pub fn event_handle(&self) -> Option<PluginEventDependencyHandle> {
330 self.event_handle.clone()
331 }
332}
333
334#[derive(Clone, Debug)]
336pub struct PluginDependencyHandle {
337 pub(super) binding: NativeEndpointBinding,
338 pub(super) caller_instance: String,
339 pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
340}
341
342#[derive(Clone, Debug)]
344pub struct PluginStreamDependencyHandle {
345 pub(super) binding: NativeStreamEndpointBinding,
346 pub(super) caller_instance: String,
347 pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
348}
349
350impl PluginStreamDependencyHandle {
351 pub fn capability_id(&self) -> &'static str {
353 self.binding.state.capability_id
354 }
355
356 pub fn descriptor_version(&self) -> &'static str {
358 self.binding.state.descriptor_version
359 }
360
361 pub fn operations(&self) -> &'static [&'static str] {
363 self.binding.state.operations
364 }
365
366 pub fn typed<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
368 if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
369 return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
370 }
371 let runtime = self
372 .runtime
373 .borrow()
374 .upgrade()
375 .ok_or(RuntimeFailure::AdmissionClosed)?;
376 Ok(NativeStreamHandle::from_endpoints(
377 std::slice::from_ref(&self.binding),
378 runtime,
379 &self.caller_instance,
380 true,
381 ))
382 }
383}
384
385impl PluginDependencyHandle {
386 pub fn capability_id(&self) -> &'static str {
388 self.binding.state.capability_id
389 }
390
391 pub fn descriptor_version(&self) -> &'static str {
393 self.binding.state.descriptor_version
394 }
395
396 pub fn operations(&self) -> &'static [&'static str] {
398 self.binding.state.operations
399 }
400
401 pub fn invoke_erased(
406 &self,
407 operation: &str,
408 request: Box<dyn Any>,
409 context: InvocationContext,
410 ) -> LocalBoxFuture<'static, Result<crate::ErasedDomainResult, RuntimeFailure>> {
411 let Some(runtime) = self.runtime.borrow().upgrade() else {
412 return Box::pin(futures::future::ready(Err(RuntimeFailure::AdmissionClosed)));
413 };
414 crate::request_handle::invoke_erased_dependency(
415 self.binding.clone(),
416 runtime,
417 self.caller_instance.clone(),
418 operation.to_owned(),
419 context,
420 request,
421 )
422 }
423
424 pub fn typed<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
426 if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
427 return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
428 }
429 let runtime = self
430 .runtime
431 .borrow()
432 .upgrade()
433 .ok_or(RuntimeFailure::AdmissionClosed)?;
434 Ok(NativeRequestHandle::from_endpoints(
435 std::slice::from_ref(&self.binding),
436 runtime,
437 &self.caller_instance,
438 true,
439 ))
440 }
441}
442
443#[derive(Clone, Debug, Default)]
445pub struct PluginDependencies {
446 pub(super) requirements: Vec<lenso_app_plan::CapabilityRequirementPlan>,
447 pub(super) bindings: Vec<PluginDependency>,
448 pub(super) caller_instance: Rc<str>,
449 pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
450}
451
452impl PluginDependencies {
453 pub(super) fn new(
454 caller_instance: impl Into<String>,
455 runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
456 requirements: Vec<lenso_app_plan::CapabilityRequirementPlan>,
457 ) -> Self {
458 Self {
459 requirements,
460 bindings: Vec::new(),
461 caller_instance: Rc::from(caller_instance.into()),
462 runtime,
463 }
464 }
465
466 pub fn requirements(&self) -> &[lenso_app_plan::CapabilityRequirementPlan] {
468 &self.requirements
469 }
470
471 pub fn requirement(&self, id: &str) -> Result<Self, RuntimeFailure> {
473 let requirement = self
474 .requirements
475 .iter()
476 .find(|requirement| requirement.requirement_id() == id)
477 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
478 detail: format!(
479 "consumer `{}` has no requirement `{id}`",
480 self.caller_instance
481 ),
482 })?;
483 Ok(Self {
484 requirements: vec![requirement.clone()],
485 bindings: self
486 .bindings
487 .iter()
488 .filter(|binding| binding.requirement_id() == id)
489 .cloned()
490 .collect(),
491 caller_instance: self.caller_instance.clone(),
492 runtime: self.runtime.clone(),
493 })
494 }
495
496 fn validate_lookup(
497 &self,
498 capability: &'static str,
499 version: &str,
500 ) -> Result<(), RuntimeFailure> {
501 let declarations = self
502 .requirements
503 .iter()
504 .filter(|requirement| requirement.capability_id() == capability)
505 .collect::<Vec<_>>();
506 match declarations.as_slice() {
507 [] => Err(RuntimeFailure::Unavailable { capability }),
508 [declaration] if declaration.descriptor_version() == version => Ok(()),
509 [_] => Err(RuntimeFailure::ProtocolViolation { capability }),
510 declarations => Err(RuntimeFailure::AmbiguousBinding {
511 capability,
512 providers: declarations.len(),
513 }),
514 }
515 }
516
517 pub fn bindings(&self) -> &[PluginDependency] {
519 &self.bindings
520 }
521
522 pub fn len(&self) -> usize {
524 self.bindings.len()
525 }
526
527 pub fn is_empty(&self) -> bool {
529 self.bindings.is_empty()
530 }
531
532 pub fn invocation_context(
538 &self,
539 deadline: Option<Duration>,
540 cancellation: CancellationToken,
541 ) -> Result<InvocationContext, RuntimeFailure> {
542 let runtime = self
543 .runtime
544 .borrow()
545 .upgrade()
546 .ok_or(RuntimeFailure::AdmissionClosed)?;
547 let request_id = runtime.request_ids.get();
548 runtime.request_ids.set(request_id.saturating_add(1));
549 Ok(InvocationContext::new(request_id, deadline, cancellation)
550 .with_shared_caller_instance(self.caller_instance.clone()))
551 }
552
553 pub fn invocation_context_after(
555 &self,
556 timeout: Duration,
557 cancellation: CancellationToken,
558 ) -> Result<InvocationContext, RuntimeFailure> {
559 let runtime = self
560 .runtime
561 .borrow()
562 .upgrade()
563 .ok_or(RuntimeFailure::AdmissionClosed)?;
564 let deadline = (runtime.driver.now)().saturating_add(timeout);
565 drop(runtime);
566 self.invocation_context(Some(deadline), cancellation)
567 }
568
569 pub(super) fn shutdown_invocation_context(
570 &self,
571 deadline: Option<Duration>,
572 cancellation: CancellationToken,
573 ) -> Result<InvocationContext, RuntimeFailure> {
574 self.invocation_context(deadline, cancellation)
575 .map(InvocationContext::for_shutdown_dependency_call)
576 }
577
578 pub fn one<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
580 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
581 let handles: Vec<_> = self
582 .bindings
583 .iter()
584 .filter(|binding| binding.capability_id() == C::ID)
585 .filter_map(PluginDependency::handle)
586 .collect();
587 match handles.as_slice() {
588 [handle] => handle.typed::<C>(),
589 [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
590 handles => Err(RuntimeFailure::AmbiguousBinding {
591 capability: C::ID,
592 providers: handles.len(),
593 }),
594 }
595 }
596
597 pub fn optional<C: RequestCapability>(
599 &self,
600 ) -> Result<Option<NativeRequestHandle<C>>, RuntimeFailure> {
601 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
602 match self
603 .bindings
604 .iter()
605 .filter(|binding| binding.capability_id() == C::ID)
606 .filter_map(PluginDependency::handle)
607 .collect::<Vec<_>>()
608 .as_slice()
609 {
610 [] => Ok(None),
611 [handle] => handle.typed::<C>().map(Some),
612 handles => Err(RuntimeFailure::AmbiguousBinding {
613 capability: C::ID,
614 providers: handles.len(),
615 }),
616 }
617 }
618
619 pub fn many<C: RequestCapability>(
621 &self,
622 ) -> Result<Vec<NativeRequestHandle<C>>, RuntimeFailure> {
623 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
624 self.bindings
625 .iter()
626 .filter(|binding| binding.capability_id() == C::ID)
627 .filter_map(PluginDependency::handle)
628 .map(|handle| handle.typed::<C>())
629 .collect()
630 }
631
632 pub fn one_stream<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
634 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
635 let handles: Vec<_> = self
636 .bindings
637 .iter()
638 .filter(|binding| binding.capability_id() == C::ID)
639 .filter_map(PluginDependency::stream_handle)
640 .collect();
641 match handles.as_slice() {
642 [handle] => handle.typed::<C>(),
643 [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
644 handles => Err(RuntimeFailure::AmbiguousBinding {
645 capability: C::ID,
646 providers: handles.len(),
647 }),
648 }
649 }
650
651 pub fn optional_stream<C: StreamCapability>(
653 &self,
654 ) -> Result<Option<NativeStreamHandle<C>>, RuntimeFailure> {
655 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
656 match self
657 .bindings
658 .iter()
659 .filter(|binding| binding.capability_id() == C::ID)
660 .filter_map(PluginDependency::stream_handle)
661 .collect::<Vec<_>>()
662 .as_slice()
663 {
664 [] => Ok(None),
665 [handle] => handle.typed::<C>().map(Some),
666 handles => Err(RuntimeFailure::AmbiguousBinding {
667 capability: C::ID,
668 providers: handles.len(),
669 }),
670 }
671 }
672
673 pub fn many_stream<C: StreamCapability>(
675 &self,
676 ) -> Result<Vec<NativeStreamHandle<C>>, RuntimeFailure> {
677 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
678 self.bindings
679 .iter()
680 .filter(|binding| binding.capability_id() == C::ID)
681 .filter_map(PluginDependency::stream_handle)
682 .map(|handle| handle.typed::<C>())
683 .collect()
684 }
685
686 pub fn many_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
688 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
689 let handles: Vec<_> = self
690 .bindings
691 .iter()
692 .filter(|binding| binding.capability_id() == C::ID)
693 .filter_map(PluginDependency::event_handle)
694 .collect();
695 if handles.iter().any(|handle| {
696 handle.capability_id() != C::ID || handle.descriptor_version() != C::DESCRIPTOR_VERSION
697 }) {
698 return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
699 }
700 let runtime = self
701 .runtime
702 .borrow()
703 .upgrade()
704 .ok_or(RuntimeFailure::AdmissionClosed)?;
705 let endpoints = handles
706 .iter()
707 .map(|handle| handle.binding.clone())
708 .collect::<Vec<_>>();
709 Ok(NativeEventHandle::from_endpoints(
710 &endpoints,
711 runtime,
712 &self.caller_instance,
713 true,
714 ))
715 }
716
717 pub fn one_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
719 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
720 match self
721 .bindings
722 .iter()
723 .filter(|binding| binding.capability_id() == C::ID)
724 .filter_map(PluginDependency::event_handle)
725 .collect::<Vec<_>>()
726 .as_slice()
727 {
728 [handle] => handle.typed::<C>(),
729 [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
730 handles => Err(RuntimeFailure::AmbiguousBinding {
731 capability: C::ID,
732 providers: handles.len(),
733 }),
734 }
735 }
736
737 pub fn optional_event<C: EventCapability>(
739 &self,
740 ) -> Result<Option<NativeEventHandle<C>>, RuntimeFailure> {
741 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
742 match self
743 .bindings
744 .iter()
745 .filter(|binding| binding.capability_id() == C::ID)
746 .filter_map(PluginDependency::event_handle)
747 .collect::<Vec<_>>()
748 .as_slice()
749 {
750 [] => Ok(None),
751 [handle] => handle.typed::<C>().map(Some),
752 handles => Err(RuntimeFailure::AmbiguousBinding {
753 capability: C::ID,
754 providers: handles.len(),
755 }),
756 }
757 }
758}