use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::Instant;
use tokio::sync::{mpsc, Notify};
use crate::envelope::EventEnvelope;
use crate::function::{AppError, ComposableFunction};
use crate::trace::{self, TraceState};
use crate::util::app_config_reader::AppConfigReader;
use crate::util::elastic_queue::{ElasticQueue, MEMORY_BUFFER};
const DISPATCH_MAILBOX_SIZE: &str = "elastic.queue.dispatch.mailbox.size";
const DEFAULT_DISPATCH_MAILBOX_SIZE: usize = 1024;
enum MailboxMessage {
Event(Box<EventEnvelope>),
Ready(usize),
}
struct RouteEntry {
private: bool,
mailbox: mpsc::Sender<MailboxMessage>,
stop: Arc<Notify>,
instances: usize,
function: Arc<dyn ComposableFunction>,
}
const RESERVED_ENGINE_ROUTES: &[&str] = &[
"event.script.manager",
"task.executor",
crate::inbox::TEMPORARY_INBOX,
];
type RouteRegistry = Arc<RwLock<HashMap<String, RouteEntry>>>;
type PoolRegistry = Arc<RwLock<HashMap<String, usize>>>;
type ShutdownHook = Box<dyn FnOnce() + Send + 'static>;
static SHUTDOWN_HOOKS: std::sync::Mutex<Vec<ShutdownHook>> = std::sync::Mutex::new(Vec::new());
static KEEP_RUNNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[derive(Clone, Default)]
pub struct Platform {
routes: RouteRegistry,
pools: PoolRegistry,
pool_mutations: Arc<Mutex<()>>,
}
const MAX_INSTANCES: usize = 1000;
#[derive(Clone, Copy, Debug, Default)]
pub struct FunctionOptions {
pub zero_traced: bool,
pub private: bool,
pub interceptor: bool,
}
impl Platform {
pub fn new() -> Self {
let platform = Self::default();
let _ = platform.register_with_options(
crate::inbox::TEMPORARY_INBOX,
Arc::new(crate::inbox::TemporaryInbox),
500,
FunctionOptions {
zero_traced: true,
interceptor: false,
private: true,
},
);
platform
}
pub fn get_instance() -> Platform {
static GLOBAL: OnceLock<Platform> = OnceLock::new();
GLOBAL.get_or_init(Platform::new).clone()
}
pub fn on_shutdown(&self, hook: impl FnOnce() + Send + 'static) {
SHUTDOWN_HOOKS
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push(Box::new(hook));
}
pub fn keep_running(&self, reason: &str) {
if !KEEP_RUNNING.swap(true, std::sync::atomic::Ordering::AcqRel) {
log::info!("{reason} keeps the application running until it is stopped");
}
}
pub fn is_kept_running() -> bool {
KEEP_RUNNING.load(std::sync::atomic::Ordering::Acquire)
}
pub fn run_shutdown_hooks() {
let hooks: Vec<ShutdownHook> = std::mem::take(
&mut *SHUTDOWN_HOOKS
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()),
);
for hook in hooks.into_iter().rev() {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(hook)).is_err() {
log::warn!("Ignorable error while running a shutdown hook");
}
}
}
pub fn name() -> String {
let config = AppConfigReader::get_instance();
config
.get_property("application.name")
.unwrap_or_else(|| "application".to_string())
}
pub fn origin() -> &'static str {
static ORIGIN: OnceLock<String> = OnceLock::new();
ORIGIN.get_or_init(|| uuid::Uuid::new_v4().simple().to_string())
}
pub fn register(
&self,
route: &str,
function: Arc<dyn ComposableFunction>,
instances: usize,
) -> Result<(), AppError> {
self.register_with_options(route, function, instances, FunctionOptions::default())
}
pub fn register_private(
&self,
route: &str,
function: Arc<dyn ComposableFunction>,
instances: usize,
) -> Result<(), AppError> {
self.register_with_options(
route,
function,
instances,
FunctionOptions {
private: true,
..FunctionOptions::default()
},
)
}
pub fn is_private(&self, route: &str) -> Option<bool> {
self.routes
.read()
.expect("route registry poisoned")
.get(route)
.map(|entry| entry.private)
}
pub fn register_with_options(
&self,
route: &str,
function: Arc<dyn ComposableFunction>,
instances: usize,
options: FunctionOptions,
) -> Result<(), AppError> {
validate_route(route)?;
let instances = instances.clamp(1, MAX_INSTANCES);
let (mailbox_tx, mailbox_rx) = mpsc::channel::<MailboxMessage>(dispatch_mailbox_size());
let stop = Arc::new(Notify::new());
{
let mut routes = self.routes.write().expect("route registry poisoned");
if let Some(previous) = routes.remove(route) {
log::warn!("Reloading LambdaFunction {route}");
previous.stop.notify_one();
}
routes.insert(
route.to_string(),
RouteEntry {
private: options.private,
mailbox: mailbox_tx.clone(),
stop: stop.clone(),
instances,
function: function.clone(),
},
);
}
let mut worker_txs = Vec::with_capacity(instances);
for instance in 1..=instances {
let (worker_tx, worker_rx) = mpsc::channel::<EventEnvelope>(1);
worker_txs.push(worker_tx);
tokio::spawn(worker_loop(
route.to_string(),
instance,
function.clone(),
worker_rx,
mailbox_tx.clone(),
self.routes.clone(),
options,
));
}
tokio::spawn(manager_loop(
route.to_string(),
mailbox_rx,
stop,
worker_txs,
));
Ok(())
}
pub fn has_route(&self, route: &str) -> bool {
self.routes
.read()
.expect("route registry poisoned")
.contains_key(route)
}
pub fn release(&self, route: &str) -> bool {
let removed = self
.routes
.write()
.expect("route registry poisoned")
.remove(route);
match removed {
Some(entry) => {
entry.stop.notify_one();
true
}
None => false,
}
}
pub fn register_route_pool(
&self,
prefix: &str,
function: Arc<dyn ComposableFunction>,
count: usize,
) -> Result<Vec<String>, AppError> {
if count < 2 {
return Err(AppError::new(400, "Route pool count must be at least 2"));
}
validate_route(&format!("{prefix}.0"))?;
let _mutation = self
.pool_mutations
.lock()
.expect("pool mutation lock poisoned");
let previous = self
.pools
.write()
.expect("pool registry poisoned")
.remove(prefix);
if let Some(previous) = previous {
log::warn!("Reloading route pool {prefix} ({previous} -> {count} lanes)");
self.release_pool_members(prefix, previous);
}
let mut members = Vec::with_capacity(count);
for n in 0..count {
let member = format!("{prefix}.{n}");
self.register_private(&member, function.clone(), 1)?;
members.push(member);
}
self.pools
.write()
.expect("pool registry poisoned")
.insert(prefix.to_string(), count);
log::info!("Route pool {prefix} with {count} instances started as async tasks");
Ok(members)
}
pub fn release_route_pool(&self, prefix: &str) -> bool {
let _mutation = self
.pool_mutations
.lock()
.expect("pool mutation lock poisoned");
let count = self
.pools
.write()
.expect("pool registry poisoned")
.remove(prefix);
match count {
Some(count) => {
self.release_pool_members(prefix, count);
log::info!("Route pool {prefix} stopped");
true
}
None => false,
}
}
fn release_pool_members(&self, prefix: &str, count: usize) {
for n in 0..count {
self.release(&format!("{prefix}.{n}"));
}
}
pub fn routes(&self) -> Vec<String> {
let mut names: Vec<String> = self
.routes
.read()
.expect("route registry poisoned")
.keys()
.cloned()
.collect();
names.sort();
names
}
pub fn instances(&self, route: &str) -> Option<usize> {
self.routes
.read()
.expect("route registry poisoned")
.get(route)
.map(|entry| entry.instances)
}
pub(crate) async fn deliver(&self, route: &str, event: EventEnvelope) -> Result<(), AppError> {
let route = bare_route(route);
let mut event = event;
normalize_null_transport(&mut event);
if let Some(function) = reserved_route_function(&self.routes, route) {
spawn_direct(function, event);
return Ok(());
}
let sender = self
.routes
.read()
.expect("route registry poisoned")
.get(route)
.map(|entry| entry.mailbox.clone());
let Some(sender) = sender else {
return Err(AppError::new(404, format!("Route {route} not found")));
};
sender
.send(MailboxMessage::Event(Box::new(event)))
.await
.map_err(|_| AppError::new(500, format!("Route {route} is closed")))
}
}
fn normalize_null_transport(event: &mut EventEnvelope) {
if !crate::serializer::null_transport() && crate::serializer::has_nil_map_entry(event.body()) {
let stripped = crate::serializer::strip_nulls_always(event.body());
event.set_body_internal(stripped);
}
}
fn reserved_route_function(
registry: &RouteRegistry,
route: &str,
) -> Option<Arc<dyn ComposableFunction>> {
if RESERVED_ENGINE_ROUTES.contains(&route) {
registry
.read()
.expect("route registry poisoned")
.get(route)
.map(|entry| entry.function.clone())
} else {
None
}
}
fn spawn_direct(function: Arc<dyn ComposableFunction>, event: EventEnvelope) {
tokio::spawn(async move {
let headers = event.headers().clone();
if let Err(e) = function.handle_event(headers, event, 1).await {
log::error!(
"Unable to execute event script - ({}) {}",
e.status(),
e.message()
);
}
});
}
fn dispatch_mailbox_size() -> usize {
let configured = AppConfigReader::get_instance()
.get_property_or(
DISPATCH_MAILBOX_SIZE,
&DEFAULT_DISPATCH_MAILBOX_SIZE.to_string(),
)
.parse::<usize>()
.unwrap_or(DEFAULT_DISPATCH_MAILBOX_SIZE);
let size = if configured > 0 {
configured
} else {
DEFAULT_DISPATCH_MAILBOX_SIZE
};
size.max(MEMORY_BUFFER as usize)
}
pub(crate) fn validate_route(route: &str) -> Result<(), AppError> {
let valid_chars = !route.is_empty()
&& route.bytes().all(|b| {
b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'-' | b'_')
});
if !valid_chars {
return Err(AppError::new(
400,
format!("Invalid route '{route}' — use lowercase letters, digits, '.', '-', '_'"),
));
}
if !route.contains('.')
|| route.starts_with('.')
|| route.ends_with('.')
|| route.contains("..")
{
return Err(AppError::new(
400,
format!("Invalid route '{route}' — a route needs at least one '.' separator (e.g. 'v1.my.function')"),
));
}
Ok(())
}
async fn manager_loop(
route: String,
mut mailbox: mpsc::Receiver<MailboxMessage>,
stop: Arc<Notify>,
worker_txs: Vec<mpsc::Sender<EventEnvelope>>,
) {
let mut elastic = ElasticQueue::new(&route);
let mut ready_fifo: VecDeque<usize> = VecDeque::new();
let mut ready_set: HashSet<usize> = HashSet::new();
let mut buffering = true;
loop {
let message = tokio::select! {
_ = stop.notified() => break,
received = mailbox.recv() => match received {
Some(message) => message,
None => break,
},
};
match message {
MailboxMessage::Ready(worker) => {
if ready_set.insert(worker) {
ready_fifo.push_back(worker);
}
if buffering {
match elastic.read() {
Ok(bytes) if bytes.is_empty() => {
buffering = false;
elastic.close();
}
Ok(bytes) => match EventEnvelope::from_bytes(&bytes) {
Ok(event) => {
if let Some(next) = ready_fifo.pop_front() {
ready_set.remove(&next);
let _ = worker_txs[next - 1].send(event).await;
}
}
Err(e) => log::error!("{route} corrupted buffered event dropped - {e}"),
},
Err(e) => log::error!("{route} dispatch error - {e}"),
}
}
}
MailboxMessage::Event(event) => {
let event = *event;
if buffering {
spill(&route, &mut elastic, &event);
} else if let Some(next) = ready_fifo.pop_front() {
ready_set.remove(&next);
let _ = worker_txs[next - 1].send(event).await;
} else {
buffering = true;
spill(&route, &mut elastic, &event);
}
}
}
}
drop(worker_txs);
elastic.destroy();
}
fn spill(route: &str, elastic: &mut ElasticQueue, event: &EventEnvelope) {
match event.to_bytes() {
Ok(bytes) => {
if let Err(e) = elastic.write(&bytes) {
log::error!("{route} dispatch error - {e}");
}
}
Err(e) => log::error!("{route} dispatch error - {e}"),
}
}
async fn worker_loop(
route: String,
instance: usize,
function: Arc<dyn ComposableFunction>,
mut events: mpsc::Receiver<EventEnvelope>,
manager: mpsc::Sender<MailboxMessage>,
registry: RouteRegistry,
options: FunctionOptions,
) {
let zero_traced = options.zero_traced || is_zero_traced(&route);
loop {
if manager.send(MailboxMessage::Ready(instance)).await.is_err() {
break; }
let Some(event) = events.recv().await else {
break;
};
let started = Instant::now();
let mut event = event;
let served_rpc = event.tag(crate::post_office::RPC_TAG).is_some();
let is_reply_listener = route == crate::inbox::TEMPORARY_INBOX;
let (business_cid, headers) = if is_reply_listener {
(None, event.headers().clone())
} else {
event.clear_annotations_internal();
let tag_cid = event
.tag(crate::post_office::BUSINESS_CID_TAG)
.map(str::to_string);
event.clear_tags_internal();
let mut headers = event.headers().clone();
headers.remove(crate::automation::X_EVENT_API);
let legacy_cid = headers.remove(crate::automation::MY_CORRELATION_ID);
let business_cid = tag_cid
.or(legacy_cid)
.or_else(|| event.correlation_id().map(str::to_string));
headers.insert(MY_ROUTE.to_string(), route.clone());
if let Some(trace_id) = event.trace_id() {
headers.insert(MY_TRACE_ID.to_string(), trace_id.to_string());
}
if let Some(trace_path) = event.trace_path() {
headers.insert(MY_TRACE_PATH.to_string(), trace_path.to_string());
}
if let Some(cid) = &business_cid {
headers.insert(
crate::automation::MY_CORRELATION_ID.to_string(),
cid.clone(),
);
}
if !options.interceptor {
for key in ENGINE_METADATA_KEYS {
event.remove_header_internal(key);
}
}
(business_cid, headers)
};
let reply_to = event.reply_to().map(str::to_string);
let cid = event.correlation_id().map(str::to_string);
let event_from = event.from().map(str::to_string);
let trace_state = match (event.trace_id(), event.trace_path()) {
(Some(trace_id), Some(trace_path)) => {
let mut state = TraceState::new(
&route,
trace_id,
trace_path,
event.span_id(),
business_cid.as_deref(),
);
state.zero_traced = zero_traced;
Some(state)
}
_ => None,
};
let (result, finished_state) =
trace::run_scoped(trace_state, function.handle_event(headers, event, instance)).await;
let elapsed_ms = started.elapsed().as_secs_f32() * 1000.0;
let elapsed_ms = (elapsed_ms.max(0.0) * 1000.0).round() / 1000.0;
let trace_triple = finished_state.as_ref().map(|s| {
(
s.trace_id.clone(),
s.trace_path.clone(),
(!s.zero_traced).then(|| s.span_id.clone()),
)
});
let reply_annotations: HashMap<String, rmpv::Value> = finished_state
.as_ref()
.filter(|s| !s.zero_traced)
.map(|s| {
s.annotations
.iter()
.filter_map(|(k, v)| {
rmpv::ext::to_value(v).ok().map(|value| (k.clone(), value))
})
.collect()
})
.unwrap_or_default();
let outcome = match &result {
Ok(response) => (response.status(), !response.has_error(), None),
Err(e) => (e.status(), false, Some(e.message().to_string())),
};
let mut not_delivered = false;
match (reply_to, result) {
(Some(_), Ok(_)) if options.interceptor => {}
(Some(reply_route), result) => {
let mut response = match result {
Ok(envelope) => envelope,
Err(e) => EventEnvelope::new()
.set_status(e.status())
.set_raw_body(rmpv::Value::String(e.message().into())),
};
response.set_cid_internal(cid);
response.set_from_internal(&route);
response.set_to_internal(&reply_route);
response.set_exec_time_internal(elapsed_ms);
response.set_annotations_internal(reply_annotations.clone());
sanitize_response_headers(&mut response);
normalize_null_transport(&mut response);
if let Some((trace_id, trace_path, span_id)) = trace_triple {
response.set_trace_internal(&trace_id, &trace_path);
match span_id {
Some(span_id) => response.set_span_id_internal(&span_id),
None => response.clear_span_id_internal(),
}
}
let reply_route = bare_route(&reply_route).to_string();
if let Some(function) = reserved_route_function(®istry, &reply_route) {
spawn_direct(function, response);
} else {
let sender = registry
.read()
.expect("route registry poisoned")
.get(&reply_route)
.map(|entry| entry.mailbox.clone());
if let Some(sender) = sender {
let _ = sender.send(MailboxMessage::Event(Box::new(response))).await;
} else {
not_delivered = true;
}
}
}
(None, Err(e)) => {
log::warn!(
"Unhandled exception in {route}#{instance}: ({}) {}",
e.status(),
e.message()
);
}
(None, Ok(_)) => {} }
if let Some(state) = finished_state.filter(|s| !s.zero_traced) {
if !served_rpc || not_delivered {
emit_telemetry(®istry, &route, event_from, &state, outcome, elapsed_ms).await;
}
}
}
}
pub(crate) fn bare_route(route: &str) -> &str {
match route.find('@') {
Some(at) => &route[..at],
None => route,
}
}
pub(crate) const MY_ROUTE: &str = "my_route";
pub(crate) const MY_TRACE_ID: &str = "my_trace_id";
pub(crate) const MY_TRACE_PATH: &str = "my_trace_path";
pub(crate) const ENGINE_METADATA_KEYS: [&str; 5] = [
MY_ROUTE,
MY_TRACE_ID,
MY_TRACE_PATH,
crate::automation::MY_CORRELATION_ID,
crate::automation::X_EVENT_API,
];
fn sanitize_response_headers(response: &mut EventEnvelope) {
for key in ENGINE_METADATA_KEYS {
response.remove_header_internal(key);
}
}
fn is_zero_traced(route: &str) -> bool {
crate::telemetry::ZERO_TRACING_FILTER.contains(&route)
}
pub(crate) fn in_skip_rpc_tracing_list(route: &str) -> bool {
AppConfigReader::get_instance()
.get_property_or("skip.rpc.tracing", "async.http.request")
.split([',', ' '])
.map(str::trim)
.any(|skipped| skipped == route)
}
async fn emit_telemetry(
registry: &RouteRegistry,
route: &str,
from: Option<String>,
state: &TraceState,
outcome: (i32, bool, Option<String>),
elapsed_ms: f32,
) {
let sender = registry
.read()
.expect("route registry poisoned")
.get(crate::telemetry::DISTRIBUTED_TRACING)
.map(|entry| entry.mailbox.clone());
let Some(sender) = sender else {
return; };
let (status, success, exception) = outcome;
let mut metrics = serde_json::Map::new();
let mut put = |k: &str, v: serde_json::Value| {
metrics.insert(k.to_string(), v);
};
put("id", serde_json::Value::String(state.trace_id.clone()));
put("path", serde_json::Value::String(state.trace_path.clone()));
put("service", serde_json::Value::String(route.to_string()));
put("start", serde_json::Value::String(state.start_time.clone()));
put(
"origin",
serde_json::Value::String(Platform::origin().to_string()),
);
put(
"exec_time",
serde_json::Value::from(((elapsed_ms as f64) * 1000.0).round() / 1000.0),
);
put("status", serde_json::Value::from(status));
put("success", serde_json::Value::Bool(success));
if let Some(exception) = exception {
put("exception", serde_json::Value::String(exception));
}
if let Some(from) = from {
put("from", serde_json::Value::String(from));
}
put("span_id", serde_json::Value::String(state.span_id.clone()));
if let Some(parent) = &state.parent_span_id {
put("parent_span_id", serde_json::Value::String(parent.clone()));
}
let mut dataset = serde_json::Map::new();
dataset.insert("trace".to_string(), serde_json::Value::Object(metrics));
if !state.annotations.is_empty() {
dataset.insert(
"annotations".to_string(),
serde_json::Value::Object(state.annotations.clone().into_iter().collect()),
);
}
match EventEnvelope::new()
.set_to(crate::telemetry::DISTRIBUTED_TRACING)
.set_body(serde_json::Value::Object(dataset))
{
Ok(event) => {
let _ = sender.send(MailboxMessage::Event(Box::new(event))).await;
}
Err(e) => log::error!("Unable to send to distributed.tracing - {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn keep_running_is_declared_once_and_read_by_the_entry_point() {
let platform = Platform::new();
platform.keep_running("test component");
platform.keep_running("test component again");
assert!(Platform::is_kept_running());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shutdown_hooks_run_newest_first_once_and_isolated() {
let order = Arc::new(std::sync::Mutex::new(Vec::new()));
let platform = Platform::new();
for tag in ["first", "second", "third"] {
let order = order.clone();
platform.on_shutdown(move || order.lock().unwrap().push(tag));
}
let boom = order.clone();
platform.on_shutdown(move || {
boom.lock().unwrap().push("boom");
panic!("a hook that fails must not stop the rest");
});
Platform::run_shutdown_hooks();
assert_eq!(
vec!["boom", "third", "second", "first"],
*order.lock().unwrap()
);
Platform::run_shutdown_hooks();
assert_eq!(4, order.lock().unwrap().len());
}
#[test]
fn route_validation_rules() {
assert!(validate_route("v1.get.profile").is_ok());
assert!(validate_route("hello.world-2_x").is_ok());
assert!(validate_route("badroute").is_err()); assert!(validate_route("UPPER.case").is_err()); assert!(validate_route(".leading.dot").is_err());
assert!(validate_route("trailing.dot.").is_err());
assert!(validate_route("double..dot").is_err());
assert!(validate_route("").is_err());
assert!(validate_route("with space.x").is_err());
}
}