use {
super::{
Functions,
accept::{CallReply, CallRequest},
status::{CandidateInfo, CandidatesMap, When},
},
crate::{
discovery::Discovery,
network::{LocalNode, UnknownPeer, error::Success, link::LinkError},
primitives::Datum,
},
builder::CallerConfig,
core::{marker::PhantomData, time::Duration},
iroh::endpoint::ApplicationClose,
std::sync::Arc,
tokio::sync::watch,
tokio_util::sync::DropGuard,
};
mod builder;
pub(super) mod worker;
pub use builder::{Builder, CallerConfig as Config};
type BoxedError = Box<dyn core::error::Error + Send + Sync + 'static>;
#[derive(Debug, thiserror::Error)]
pub enum CallError<E> {
#[error("function returned an application error")]
Application(E),
#[error("call timed out")]
Timeout,
#[error("no eligible handler available")]
Unavailable,
#[error("call rejected by handler: {}", .0.reason.escape_ascii())]
Rejected(ApplicationClose),
#[error("transport failure: {0}")]
Transport(#[source] BoxedError),
#[error("request encoding failed: {0}")]
Encode(#[source] BoxedError),
#[error("response decoding failed: {0}")]
Decode(#[source] BoxedError),
}
pub struct Caller<Req: Datum, Res: Datum, E: Datum = ()> {
config: Arc<CallerConfig>,
local: LocalNode,
discovery: Discovery,
candidates: watch::Receiver<CandidatesMap>,
status: When,
metrics_labels: [(&'static str, String); 2],
_abort: DropGuard,
_marker: PhantomData<fn(&Req, &Res, &E)>,
}
impl<Req: Datum, Res: Datum, E: Datum> Caller<Req, Res, E> {
pub fn function_id(&self) -> &super::FunctionId {
&self.config.function_id
}
pub const fn when(&self) -> &When {
&self.status
}
pub async fn call(&self, req: Req) -> Result<Res, CallError<E>> {
self.call_with_timeout(req, self.config.call_timeout).await
}
pub async fn call_with_timeout(
&self,
req: Req,
timeout: Duration,
) -> Result<Res, CallError<E>> {
let payload = req.encode().map_err(|e| CallError::Encode(Box::new(e)))?;
let request = CallRequest {
network_id: *self.local.network_id(),
function_id: self.config.function_id,
payload,
};
let mut state = CallState::default();
let attempts = self.run_attempts(&request, &mut state);
match tokio::time::timeout(timeout, attempts).await {
Ok(result) => result,
Err(_elapsed) => Err(state.into_timeout_error()),
}
}
async fn run_attempts(
&self,
request: &CallRequest,
state: &mut CallState,
) -> Result<Res, CallError<E>> {
let mut candidates = self.candidates.clone();
candidates.mark_changed();
loop {
if candidates.changed().await.is_err() {
return Err(CallError::Unavailable);
}
let snapshot = candidates.borrow_and_update().clone();
let mut ranked: Vec<CandidateInfo> = snapshot.values().cloned().collect();
ranked.sort_by_key(|c| (c.rtt().is_none(), c.rtt()));
if ranked.is_empty() {
continue;
}
state.had_candidates = true;
for candidate in &ranked {
if candidate
.entry()
.validate_tickets(&self.config.handler_auth)
.is_err()
{
continue;
}
match self.invoke_once(candidate, request).await {
Ok(CallReply::Ok(bytes)) => {
metrics::counter!(
"mosaik.functions.calls.completed",
&self.metrics_labels
)
.increment(1);
return Res::decode(&bytes)
.map_err(|e| CallError::Decode(Box::new(e)));
}
Ok(CallReply::Err(bytes)) => {
metrics::counter!(
"mosaik.functions.calls.completed",
&self.metrics_labels
)
.increment(1);
return match E::decode(&bytes) {
Ok(app_err) => Err(CallError::Application(app_err)),
Err(e) => Err(CallError::Decode(Box::new(e))),
};
}
Err(Attempt::Rejected(reason)) => {
state.last_rejection = Some(reason);
}
Err(Attempt::Transport(error)) => {
state.last_transport = Some(error);
}
}
}
}
}
async fn invoke_once(
&self,
candidate: &CandidateInfo,
request: &CallRequest,
) -> Result<CallReply, Attempt> {
match self.attempt(candidate, request).await {
Err(Attempt::Rejected(reason)) if reason == UnknownPeer => {
let addr = candidate.entry().address().clone();
let _ = self.discovery.sync_with(addr).await;
self.attempt(candidate, request).await
}
other => other,
}
}
async fn attempt(
&self,
candidate: &CandidateInfo,
request: &CallRequest,
) -> Result<CallReply, Attempt> {
metrics::counter!("mosaik.functions.calls.attempts", &self.metrics_labels)
.increment(1);
let mut link = self
.local
.connect::<Functions>(candidate.entry().address().clone())
.await
.map_err(|e| Attempt::classify(e.into()))?;
link
.send(request)
.await
.map_err(|e| Attempt::classify(e.into()))?;
let reply: CallReply =
link.recv().await.map_err(|e| Attempt::classify(e.into()))?;
let _ = link.close(Success).await;
Ok(reply)
}
}
impl<Req: Datum, Res: Datum, E: Datum> core::fmt::Debug
for Caller<Req, Res, E>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Caller")
.field("function_id", &self.config.function_id)
.finish_non_exhaustive()
}
}
enum Attempt {
Rejected(ApplicationClose),
Transport(LinkError),
}
impl Attempt {
fn classify(error: LinkError) -> Self {
if let Some(reason) = error.close_reason() {
return Self::Rejected(reason.clone());
}
Self::Transport(error)
}
}
#[derive(Default)]
struct CallState {
had_candidates: bool,
last_rejection: Option<ApplicationClose>,
last_transport: Option<LinkError>,
}
impl CallState {
fn into_timeout_error<E>(self) -> CallError<E> {
if !self.had_candidates {
return CallError::Unavailable;
}
if let Some(reason) = self.last_rejection {
return CallError::Rejected(reason);
}
if let Some(error) = self.last_transport {
return CallError::Transport(Box::new(error));
}
CallError::Timeout
}
}