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 child_context(
369 &self,
370 context: InvocationContext,
371 ) -> Result<InvocationContext, RuntimeFailure> {
372 let runtime = self
373 .runtime
374 .borrow()
375 .upgrade()
376 .ok_or(RuntimeFailure::AdmissionClosed)?;
377 let request_id = runtime.request_ids.get();
378 runtime.request_ids.set(request_id.saturating_add(1));
379 Ok(context.for_child_request(request_id))
380 }
381
382 pub fn typed<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
384 if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
385 return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
386 }
387 let runtime = self
388 .runtime
389 .borrow()
390 .upgrade()
391 .ok_or(RuntimeFailure::AdmissionClosed)?;
392 Ok(NativeStreamHandle::from_endpoints(
393 std::slice::from_ref(&self.binding),
394 runtime,
395 &self.caller_instance,
396 true,
397 ))
398 }
399}
400
401impl PluginDependencyHandle {
402 pub fn capability_id(&self) -> &'static str {
404 self.binding.state.capability_id
405 }
406
407 pub fn descriptor_version(&self) -> &'static str {
409 self.binding.state.descriptor_version
410 }
411
412 pub fn operations(&self) -> &'static [&'static str] {
414 self.binding.state.operations
415 }
416
417 pub fn invoke_erased(
422 &self,
423 operation: &str,
424 request: Box<dyn Any>,
425 context: InvocationContext,
426 ) -> LocalBoxFuture<'static, Result<crate::ErasedDomainResult, RuntimeFailure>> {
427 let Some(runtime) = self.runtime.borrow().upgrade() else {
428 return Box::pin(futures::future::ready(Err(RuntimeFailure::AdmissionClosed)));
429 };
430 crate::request_handle::invoke_erased_dependency(
431 self.binding.clone(),
432 runtime,
433 self.caller_instance.clone(),
434 operation.to_owned(),
435 context,
436 request,
437 )
438 }
439
440 pub fn typed<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
442 if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
443 return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
444 }
445 let runtime = self
446 .runtime
447 .borrow()
448 .upgrade()
449 .ok_or(RuntimeFailure::AdmissionClosed)?;
450 Ok(NativeRequestHandle::from_endpoints(
451 std::slice::from_ref(&self.binding),
452 runtime,
453 &self.caller_instance,
454 true,
455 ))
456 }
457}
458
459#[derive(Clone, Debug, Default)]
461pub struct PluginDependencies {
462 pub(super) requirements: Vec<lenso_app_plan::CapabilityRequirementPlan>,
463 pub(super) bindings: Vec<PluginDependency>,
464 pub(super) caller_instance: Rc<str>,
465 pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
466}
467
468impl PluginDependencies {
469 pub(super) fn new(
470 caller_instance: impl Into<String>,
471 runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
472 requirements: Vec<lenso_app_plan::CapabilityRequirementPlan>,
473 ) -> Self {
474 Self {
475 requirements,
476 bindings: Vec::new(),
477 caller_instance: Rc::from(caller_instance.into()),
478 runtime,
479 }
480 }
481
482 pub fn requirements(&self) -> &[lenso_app_plan::CapabilityRequirementPlan] {
484 &self.requirements
485 }
486
487 pub fn requirement(&self, id: &str) -> Result<Self, RuntimeFailure> {
489 let requirement = self
490 .requirements
491 .iter()
492 .find(|requirement| requirement.requirement_id() == id)
493 .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
494 detail: format!(
495 "consumer `{}` has no requirement `{id}`",
496 self.caller_instance
497 ),
498 })?;
499 Ok(Self {
500 requirements: vec![requirement.clone()],
501 bindings: self
502 .bindings
503 .iter()
504 .filter(|binding| binding.requirement_id() == id)
505 .cloned()
506 .collect(),
507 caller_instance: self.caller_instance.clone(),
508 runtime: self.runtime.clone(),
509 })
510 }
511
512 fn validate_lookup(
513 &self,
514 capability: &'static str,
515 version: &str,
516 ) -> Result<(), RuntimeFailure> {
517 let declarations = self
518 .requirements
519 .iter()
520 .filter(|requirement| requirement.capability_id() == capability)
521 .collect::<Vec<_>>();
522 match declarations.as_slice() {
523 [] => Err(RuntimeFailure::Unavailable { capability }),
524 [declaration] if declaration.descriptor_version() == version => Ok(()),
525 [_] => Err(RuntimeFailure::ProtocolViolation { capability }),
526 declarations => Err(RuntimeFailure::AmbiguousBinding {
527 capability,
528 providers: declarations.len(),
529 }),
530 }
531 }
532
533 pub fn bindings(&self) -> &[PluginDependency] {
535 &self.bindings
536 }
537
538 pub fn len(&self) -> usize {
540 self.bindings.len()
541 }
542
543 pub fn is_empty(&self) -> bool {
545 self.bindings.is_empty()
546 }
547
548 pub fn invocation_context(
554 &self,
555 deadline: Option<Duration>,
556 cancellation: CancellationToken,
557 ) -> Result<InvocationContext, RuntimeFailure> {
558 let runtime = self
559 .runtime
560 .borrow()
561 .upgrade()
562 .ok_or(RuntimeFailure::AdmissionClosed)?;
563 let request_id = runtime.request_ids.get();
564 runtime.request_ids.set(request_id.saturating_add(1));
565 Ok(InvocationContext::new(request_id, deadline, cancellation)
566 .with_shared_caller_instance(self.caller_instance.clone()))
567 }
568
569 pub fn invocation_context_after(
571 &self,
572 timeout: Duration,
573 cancellation: CancellationToken,
574 ) -> Result<InvocationContext, RuntimeFailure> {
575 let runtime = self
576 .runtime
577 .borrow()
578 .upgrade()
579 .ok_or(RuntimeFailure::AdmissionClosed)?;
580 let deadline = (runtime.driver.now)().saturating_add(timeout);
581 drop(runtime);
582 self.invocation_context(Some(deadline), cancellation)
583 }
584
585 pub(super) fn shutdown_invocation_context(
586 &self,
587 deadline: Option<Duration>,
588 cancellation: CancellationToken,
589 ) -> Result<InvocationContext, RuntimeFailure> {
590 self.invocation_context(deadline, cancellation)
591 .map(InvocationContext::for_shutdown_dependency_call)
592 }
593
594 pub fn one<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
596 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
597 let handles: Vec<_> = self
598 .bindings
599 .iter()
600 .filter(|binding| binding.capability_id() == C::ID)
601 .filter_map(PluginDependency::handle)
602 .collect();
603 match handles.as_slice() {
604 [handle] => handle.typed::<C>(),
605 [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
606 handles => Err(RuntimeFailure::AmbiguousBinding {
607 capability: C::ID,
608 providers: handles.len(),
609 }),
610 }
611 }
612
613 pub fn optional<C: RequestCapability>(
615 &self,
616 ) -> Result<Option<NativeRequestHandle<C>>, RuntimeFailure> {
617 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
618 match self
619 .bindings
620 .iter()
621 .filter(|binding| binding.capability_id() == C::ID)
622 .filter_map(PluginDependency::handle)
623 .collect::<Vec<_>>()
624 .as_slice()
625 {
626 [] => Ok(None),
627 [handle] => handle.typed::<C>().map(Some),
628 handles => Err(RuntimeFailure::AmbiguousBinding {
629 capability: C::ID,
630 providers: handles.len(),
631 }),
632 }
633 }
634
635 pub fn many<C: RequestCapability>(
637 &self,
638 ) -> Result<Vec<NativeRequestHandle<C>>, RuntimeFailure> {
639 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
640 self.bindings
641 .iter()
642 .filter(|binding| binding.capability_id() == C::ID)
643 .filter_map(PluginDependency::handle)
644 .map(|handle| handle.typed::<C>())
645 .collect()
646 }
647
648 pub fn one_stream<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
650 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
651 let handles: Vec<_> = self
652 .bindings
653 .iter()
654 .filter(|binding| binding.capability_id() == C::ID)
655 .filter_map(PluginDependency::stream_handle)
656 .collect();
657 match handles.as_slice() {
658 [handle] => handle.typed::<C>(),
659 [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
660 handles => Err(RuntimeFailure::AmbiguousBinding {
661 capability: C::ID,
662 providers: handles.len(),
663 }),
664 }
665 }
666
667 pub fn optional_stream<C: StreamCapability>(
669 &self,
670 ) -> Result<Option<NativeStreamHandle<C>>, RuntimeFailure> {
671 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
672 match self
673 .bindings
674 .iter()
675 .filter(|binding| binding.capability_id() == C::ID)
676 .filter_map(PluginDependency::stream_handle)
677 .collect::<Vec<_>>()
678 .as_slice()
679 {
680 [] => Ok(None),
681 [handle] => handle.typed::<C>().map(Some),
682 handles => Err(RuntimeFailure::AmbiguousBinding {
683 capability: C::ID,
684 providers: handles.len(),
685 }),
686 }
687 }
688
689 pub fn many_stream<C: StreamCapability>(
691 &self,
692 ) -> Result<Vec<NativeStreamHandle<C>>, RuntimeFailure> {
693 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
694 self.bindings
695 .iter()
696 .filter(|binding| binding.capability_id() == C::ID)
697 .filter_map(PluginDependency::stream_handle)
698 .map(|handle| handle.typed::<C>())
699 .collect()
700 }
701
702 pub fn many_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
704 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
705 let handles: Vec<_> = self
706 .bindings
707 .iter()
708 .filter(|binding| binding.capability_id() == C::ID)
709 .filter_map(PluginDependency::event_handle)
710 .collect();
711 if handles.iter().any(|handle| {
712 handle.capability_id() != C::ID || handle.descriptor_version() != C::DESCRIPTOR_VERSION
713 }) {
714 return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
715 }
716 let runtime = self
717 .runtime
718 .borrow()
719 .upgrade()
720 .ok_or(RuntimeFailure::AdmissionClosed)?;
721 let endpoints = handles
722 .iter()
723 .map(|handle| handle.binding.clone())
724 .collect::<Vec<_>>();
725 Ok(NativeEventHandle::from_endpoints(
726 &endpoints,
727 runtime,
728 &self.caller_instance,
729 true,
730 ))
731 }
732
733 pub fn one_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
735 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
736 match self
737 .bindings
738 .iter()
739 .filter(|binding| binding.capability_id() == C::ID)
740 .filter_map(PluginDependency::event_handle)
741 .collect::<Vec<_>>()
742 .as_slice()
743 {
744 [handle] => handle.typed::<C>(),
745 [] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
746 handles => Err(RuntimeFailure::AmbiguousBinding {
747 capability: C::ID,
748 providers: handles.len(),
749 }),
750 }
751 }
752
753 pub fn optional_event<C: EventCapability>(
755 &self,
756 ) -> Result<Option<NativeEventHandle<C>>, RuntimeFailure> {
757 self.validate_lookup(C::ID, C::DESCRIPTOR_VERSION)?;
758 match self
759 .bindings
760 .iter()
761 .filter(|binding| binding.capability_id() == C::ID)
762 .filter_map(PluginDependency::event_handle)
763 .collect::<Vec<_>>()
764 .as_slice()
765 {
766 [] => Ok(None),
767 [handle] => handle.typed::<C>().map(Some),
768 handles => Err(RuntimeFailure::AmbiguousBinding {
769 capability: C::ID,
770 providers: handles.len(),
771 }),
772 }
773 }
774}