use std::time::Duration;
use super::{
CancellationToken, EventCapability, InvocationContext, LocalBoxFuture,
ModuleEventDependencyHandle, NativeAppRuntime, NativeEndpointBinding, NativeEventHandle,
NativeRequestEndpoint, NativeRequestHandle, NativeStreamEndpointBinding, NativeStreamHandle,
Rc, RefCell, StreamCapability, Weak,
};
pub trait RequestCapability: 'static {
type Request: 'static;
type Response: 'static;
type DomainError: 'static;
const ID: &'static str;
const DESCRIPTOR_VERSION: &'static str;
#[doc(hidden)]
fn invoke_native(
endpoint: &dyn NativeRequestEndpoint,
operation: &str,
request: Self::Request,
context: InvocationContext,
) -> NativeRequestFuture<Self>
where
Self: Sized,
{
invoke_typed_or_erased_native_request::<Self>(endpoint, operation, request, context)
}
}
#[doc(hidden)]
pub type NativeRequestFuture<C> = LocalBoxFuture<
'static,
Result<
Result<<C as RequestCapability>::Response, <C as RequestCapability>::DomainError>,
RuntimeFailure,
>,
>;
type TypedNativeRequestFn<C> =
dyn Fn(&str, <C as RequestCapability>::Request, InvocationContext) -> NativeRequestFuture<C>;
#[doc(hidden)]
pub struct TypedNativeRequestEndpoint<C: RequestCapability> {
invoke: Rc<TypedNativeRequestFn<C>>,
}
impl<C: RequestCapability> TypedNativeRequestEndpoint<C> {
pub fn new(
invoke: impl Fn(&str, C::Request, InvocationContext) -> NativeRequestFuture<C> + 'static,
) -> Self {
Self {
invoke: Rc::new(invoke),
}
}
pub fn invoke(
&self,
operation: &str,
request: C::Request,
context: InvocationContext,
) -> NativeRequestFuture<C> {
(self.invoke)(operation, request, context)
}
}
impl<C: RequestCapability> std::fmt::Debug for TypedNativeRequestEndpoint<C> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TypedNativeRequestEndpoint")
.field("capability", &C::ID)
.finish_non_exhaustive()
}
}
#[doc(hidden)]
pub fn invoke_typed_or_erased_native_request<C: RequestCapability>(
endpoint: &dyn NativeRequestEndpoint,
operation: &str,
request: C::Request,
context: InvocationContext,
) -> NativeRequestFuture<C> {
if let Some(endpoint) = endpoint
.typed_endpoint()
.and_then(|endpoint| endpoint.downcast_ref::<TypedNativeRequestEndpoint<C>>())
{
endpoint.invoke(operation, request, context)
} else {
invoke_erased_native_request::<C>(endpoint, operation, request, context)
}
}
#[doc(hidden)]
pub fn invoke_erased_native_request<C: RequestCapability>(
endpoint: &dyn NativeRequestEndpoint,
operation: &str,
request: C::Request,
context: InvocationContext,
) -> NativeRequestFuture<C> {
let invocation = endpoint.invoke(operation, Box::new(request), context);
Box::pin(async move {
match invocation.await? {
Ok(value) => value
.downcast::<C::Response>()
.map(|value| Ok(*value))
.map_err(|_| RuntimeFailure::ProtocolViolation { capability: C::ID }),
Err(value) => value
.downcast::<C::DomainError>()
.map(|value| Err(*value))
.map_err(|_| RuntimeFailure::ProtocolViolation { capability: C::ID }),
}
})
}
pub type RequestId = u64;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RuntimeFailure {
Unavailable { capability: &'static str },
UnknownOperation {
capability: &'static str,
operation: String,
},
AmbiguousBinding {
capability: &'static str,
providers: usize,
},
ProtocolViolation { capability: &'static str },
MissingModuleFactory {
instance: String,
package_id: String,
},
UnavailableExecutionClass {
instance_key: String,
execution_class: String,
},
InvalidResolvedPlan { detail: String },
AdmissionClosed,
ResourceExhausted {
capability: &'static str,
operation: String,
},
DeadlineExceeded { request_id: RequestId },
Cancelled { request_id: RequestId },
Internal { detail: String },
ModuleFailure { detail: String },
ModuleRestartExhausted { instance: String, attempts: usize },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ModuleLifecyclePhase {
Prepare,
Activate,
Ready,
Deactivate,
}
#[cfg(test)]
mod typed_endpoint_tests {
use std::any::Any;
use super::*;
#[derive(Debug)]
struct Echo;
impl RequestCapability for Echo {
type Request = u64;
type Response = u64;
type DomainError = ();
const ID: &'static str = "test.echo@1";
const DESCRIPTOR_VERSION: &'static str = "1.0.0";
}
#[derive(Debug)]
struct Endpoint {
typed: TypedNativeRequestEndpoint<Echo>,
}
impl NativeRequestEndpoint for Endpoint {
fn capability_id(&self) -> &'static str {
Echo::ID
}
fn descriptor_version(&self) -> &'static str {
Echo::DESCRIPTOR_VERSION
}
fn operations(&self) -> &'static [&'static str] {
&["echo"]
}
fn typed_endpoint(&self) -> Option<&dyn Any> {
Some(&self.typed)
}
fn invoke(
&self,
_operation: &str,
_request: Box<dyn Any>,
_context: InvocationContext,
) -> LocalBoxFuture<'static, Result<crate::ErasedDomainResult, RuntimeFailure>> {
panic!("typed dispatch must not call the erased endpoint")
}
}
#[test]
fn default_dispatch_uses_runtime_typed_endpoint() {
let endpoint = Endpoint {
typed: TypedNativeRequestEndpoint::new(|_, request, _| {
Box::pin(futures::future::ready(Ok(Ok(request + 1))))
}),
};
let context = InvocationContext::new(1, None, CancellationToken::new());
let result =
futures::executor::block_on(Echo::invoke_native(&endpoint, "echo", 41, context));
assert_eq!(result, Ok(Ok(42)));
}
}
#[derive(Clone, Debug)]
pub struct ModuleDependency {
pub(super) capability_id: String,
pub(super) provider_instance: String,
pub(super) provider_order: usize,
pub(super) handle: Option<ModuleDependencyHandle>,
pub(super) stream_handle: Option<ModuleStreamDependencyHandle>,
pub(super) event_handle: Option<ModuleEventDependencyHandle>,
}
impl ModuleDependency {
pub(super) fn new(
capability_id: impl Into<String>,
provider_instance: impl Into<String>,
provider_order: usize,
handle: Option<ModuleDependencyHandle>,
stream_handle: Option<ModuleStreamDependencyHandle>,
event_handle: Option<ModuleEventDependencyHandle>,
) -> Self {
Self {
capability_id: capability_id.into(),
provider_instance: provider_instance.into(),
provider_order,
handle,
stream_handle,
event_handle,
}
}
pub fn capability_id(&self) -> &str {
&self.capability_id
}
pub fn provider_instance(&self) -> &str {
&self.provider_instance
}
pub const fn provider_order(&self) -> usize {
self.provider_order
}
pub fn handle(&self) -> Option<ModuleDependencyHandle> {
self.handle.clone()
}
pub fn stream_handle(&self) -> Option<ModuleStreamDependencyHandle> {
self.stream_handle.clone()
}
pub fn event_handle(&self) -> Option<ModuleEventDependencyHandle> {
self.event_handle.clone()
}
}
#[derive(Clone, Debug)]
pub struct ModuleDependencyHandle {
pub(super) binding: NativeEndpointBinding,
pub(super) caller_instance: String,
pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
}
#[derive(Clone, Debug)]
pub struct ModuleStreamDependencyHandle {
pub(super) binding: NativeStreamEndpointBinding,
pub(super) caller_instance: String,
pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
}
impl ModuleStreamDependencyHandle {
pub fn capability_id(&self) -> &'static str {
self.binding.state.capability_id
}
pub fn descriptor_version(&self) -> &'static str {
self.binding.state.descriptor_version
}
pub fn operations(&self) -> &'static [&'static str] {
self.binding.state.operations
}
pub fn typed<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
}
let runtime = self
.runtime
.borrow()
.upgrade()
.ok_or(RuntimeFailure::AdmissionClosed)?;
Ok(NativeStreamHandle::from_endpoints(
std::slice::from_ref(&self.binding),
runtime,
&self.caller_instance,
true,
))
}
}
impl ModuleDependencyHandle {
pub fn capability_id(&self) -> &'static str {
self.binding.state.capability_id
}
pub fn descriptor_version(&self) -> &'static str {
self.binding.state.descriptor_version
}
pub fn operations(&self) -> &'static [&'static str] {
self.binding.state.operations
}
pub fn typed<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
if self.capability_id() != C::ID || self.descriptor_version() != C::DESCRIPTOR_VERSION {
return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
}
let runtime = self
.runtime
.borrow()
.upgrade()
.ok_or(RuntimeFailure::AdmissionClosed)?;
Ok(NativeRequestHandle::from_endpoints(
std::slice::from_ref(&self.binding),
runtime,
&self.caller_instance,
true,
))
}
}
#[derive(Clone, Debug, Default)]
pub struct ModuleDependencies {
pub(super) bindings: Vec<ModuleDependency>,
pub(super) caller_instance: Rc<str>,
pub(super) runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
}
impl ModuleDependencies {
pub(super) fn new(
caller_instance: impl Into<String>,
runtime: Rc<RefCell<Weak<NativeAppRuntime>>>,
) -> Self {
Self {
bindings: Vec::new(),
caller_instance: Rc::from(caller_instance.into()),
runtime,
}
}
pub fn bindings(&self) -> &[ModuleDependency] {
&self.bindings
}
pub fn len(&self) -> usize {
self.bindings.len()
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
pub fn invocation_context(
&self,
deadline: Option<Duration>,
cancellation: CancellationToken,
) -> Result<InvocationContext, RuntimeFailure> {
let runtime = self
.runtime
.borrow()
.upgrade()
.ok_or(RuntimeFailure::AdmissionClosed)?;
let request_id = runtime.request_ids.get();
runtime.request_ids.set(request_id.saturating_add(1));
Ok(InvocationContext::new(request_id, deadline, cancellation)
.with_shared_caller_instance(self.caller_instance.clone()))
}
pub fn invocation_context_after(
&self,
timeout: Duration,
cancellation: CancellationToken,
) -> Result<InvocationContext, RuntimeFailure> {
let runtime = self
.runtime
.borrow()
.upgrade()
.ok_or(RuntimeFailure::AdmissionClosed)?;
let deadline = (runtime.driver.now)().saturating_add(timeout);
drop(runtime);
self.invocation_context(Some(deadline), cancellation)
}
pub fn one<C: RequestCapability>(&self) -> Result<NativeRequestHandle<C>, RuntimeFailure> {
let handles: Vec<_> = self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::handle)
.collect();
match handles.as_slice() {
[handle] => handle.typed::<C>(),
[] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
handles => Err(RuntimeFailure::AmbiguousBinding {
capability: C::ID,
providers: handles.len(),
}),
}
}
pub fn optional<C: RequestCapability>(
&self,
) -> Result<Option<NativeRequestHandle<C>>, RuntimeFailure> {
match self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::handle)
.collect::<Vec<_>>()
.as_slice()
{
[] => Ok(None),
[handle] => handle.typed::<C>().map(Some),
handles => Err(RuntimeFailure::AmbiguousBinding {
capability: C::ID,
providers: handles.len(),
}),
}
}
pub fn many<C: RequestCapability>(
&self,
) -> Result<Vec<NativeRequestHandle<C>>, RuntimeFailure> {
self.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::handle)
.map(|handle| handle.typed::<C>())
.collect()
}
pub fn one_stream<C: StreamCapability>(&self) -> Result<NativeStreamHandle<C>, RuntimeFailure> {
let handles: Vec<_> = self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::stream_handle)
.collect();
match handles.as_slice() {
[handle] => handle.typed::<C>(),
[] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
handles => Err(RuntimeFailure::AmbiguousBinding {
capability: C::ID,
providers: handles.len(),
}),
}
}
pub fn optional_stream<C: StreamCapability>(
&self,
) -> Result<Option<NativeStreamHandle<C>>, RuntimeFailure> {
match self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::stream_handle)
.collect::<Vec<_>>()
.as_slice()
{
[] => Ok(None),
[handle] => handle.typed::<C>().map(Some),
handles => Err(RuntimeFailure::AmbiguousBinding {
capability: C::ID,
providers: handles.len(),
}),
}
}
pub fn many_stream<C: StreamCapability>(
&self,
) -> Result<Vec<NativeStreamHandle<C>>, RuntimeFailure> {
self.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::stream_handle)
.map(|handle| handle.typed::<C>())
.collect()
}
pub fn many_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
let handles: Vec<_> = self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::event_handle)
.collect();
if handles.iter().any(|handle| {
handle.capability_id() != C::ID || handle.descriptor_version() != C::DESCRIPTOR_VERSION
}) {
return Err(RuntimeFailure::ProtocolViolation { capability: C::ID });
}
let runtime = self
.runtime
.borrow()
.upgrade()
.ok_or(RuntimeFailure::AdmissionClosed)?;
let endpoints = handles
.iter()
.map(|handle| handle.binding.clone())
.collect::<Vec<_>>();
Ok(NativeEventHandle::from_endpoints(
&endpoints,
runtime,
&self.caller_instance,
true,
))
}
pub fn one_event<C: EventCapability>(&self) -> Result<NativeEventHandle<C>, RuntimeFailure> {
match self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::event_handle)
.collect::<Vec<_>>()
.as_slice()
{
[handle] => handle.typed::<C>(),
[] => Err(RuntimeFailure::Unavailable { capability: C::ID }),
handles => Err(RuntimeFailure::AmbiguousBinding {
capability: C::ID,
providers: handles.len(),
}),
}
}
pub fn optional_event<C: EventCapability>(
&self,
) -> Result<Option<NativeEventHandle<C>>, RuntimeFailure> {
match self
.bindings
.iter()
.filter(|binding| binding.capability_id() == C::ID)
.filter_map(ModuleDependency::event_handle)
.collect::<Vec<_>>()
.as_slice()
{
[] => Ok(None),
[handle] => handle.typed::<C>().map(Some),
handles => Err(RuntimeFailure::AmbiguousBinding {
capability: C::ID,
providers: handles.len(),
}),
}
}
}