use std::collections::BTreeMap;
use std::future::Future;
use std::net::SocketAddr;
use std::sync::Arc;
use arc_swap::ArcSwap;
use bytes::Bytes;
use futures::channel::oneshot;
use proxima_telemetry::warn;
use serde_json::Value;
use proxima_primitives::pipe::SendPipe;
use crate::context_inject::ContextInjector;
use crate::error::ProximaError;
#[cfg(any(feature = "http1", feature = "http1-native"))]
use crate::listeners::http::HttpListenProtocol;
#[cfg(feature = "tokio")]
use crate::listeners::mcp::McpListenProtocol;
use crate::load::{LoadContext, ResolvedSpec, Spec, load};
use crate::mount::{MethodFilter, Mount, Router};
use crate::pipe::{Handler, PipeHandle, into_handle};
use crate::request::{Request, Response};
use crate::runtime::{Runtime, RuntimeSelection};
use crate::telemetry::{Metrics, TelemetryHandle};
use proxima_listen::ListenRegistry;
use proxima_listen::handle::{ListenerHandle, ListenerSpec};
use proxima_primitives::stream::{AcceptorFactory, DatagramFactory, UnixUpstreamFactory};
pub struct App {
pipes: BTreeMap<String, PipeHandle>,
pipe_specs: BTreeMap<String, Value>,
sources: BTreeMap<String, proxima_primitives::pipe::SourceHandle>,
router: Arc<ArcSwap<Router>>,
load_context: LoadContext,
listen_registry: Arc<ListenRegistry>,
runtime: Option<Arc<dyn crate::runtime::Runtime>>,
acceptor_factory: Option<Arc<dyn AcceptorFactory>>,
datagram_factory: Option<Arc<dyn DatagramFactory>>,
unix_upstream_factory: Option<Arc<dyn UnixUpstreamFactory>>,
packet_listener_factory: Option<Arc<dyn proxima_net::packet::PacketListenerFactory>>,
#[cfg(feature = "any-listener")]
any_registry: Arc<proxima_listen::any::AnyRegistry>,
#[cfg(feature = "any-listener")]
any_default_handlers: Arc<ArcSwap<BTreeMap<String, proxima_listen::any::AnyHandler>>>,
}
#[cfg(any(
feature = "runtime-tokio",
all(
feature = "serve-prime",
feature = "runtime-prime-reactor",
any(target_os = "linux", target_os = "macos", target_os = "windows")
)
))]
fn resolve_cores(explicit: Option<usize>) -> Result<usize, ProximaError> {
if let Some(cores) = explicit {
return Ok(cores.max(1));
}
Ok(crate::app_config::RuntimeConfig::resolve_from_env()?.resolved_cores())
}
fn resolve_runtime_selection(
explicit: Option<RuntimeSelection>,
cores_override: Option<usize>,
) -> Result<Option<RuntimeSelection>, ProximaError> {
if let Some(selection) = explicit {
return Ok(Some(selection));
}
if let Some(installed) = crate::runtime::installed_runtime() {
return Ok(Some(installed));
}
resolve_default_runtime_selection(cores_override)
}
type RuntimeSelectionFields = (
Option<Arc<dyn Runtime>>,
Option<Arc<dyn AcceptorFactory>>,
Option<Arc<dyn DatagramFactory>>,
Option<Arc<dyn UnixUpstreamFactory>>,
Option<Arc<dyn proxima_net::packet::PacketListenerFactory>>,
);
fn split_runtime_selection(selection: Option<RuntimeSelection>) -> RuntimeSelectionFields {
match selection {
Some(selection) => (
Some(selection.runtime),
Some(selection.acceptor_factory),
selection.datagram_factory,
selection.unix_upstream_factory,
selection.packet_listener_factory,
),
None => (None, None, None, None, None),
}
}
#[allow(clippy::needless_return)]
fn resolve_default_runtime_selection(
cores_override: Option<usize>,
) -> Result<Option<RuntimeSelection>, ProximaError> {
#[cfg(all(
feature = "serve-prime",
feature = "runtime-prime-reactor",
feature = "tokio",
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
{
let runtime: Arc<dyn Runtime> = Arc::new(
crate::runtime::PrimeRuntime::new_with_tokio_compat(resolve_cores(cores_override)?)?,
);
return Ok(Some(RuntimeSelection::from_prime(runtime)));
}
#[cfg(all(
feature = "serve-prime",
feature = "runtime-prime-reactor",
not(feature = "tokio"),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
{
let runtime: Arc<dyn Runtime> = Arc::new(crate::runtime::PrimeRuntime::new(
resolve_cores(cores_override)?,
)?);
return Ok(Some(RuntimeSelection::from_prime(runtime)));
}
#[cfg(all(
feature = "runtime-tokio",
not(all(
feature = "serve-prime",
feature = "runtime-prime-reactor",
any(target_os = "linux", target_os = "macos", target_os = "windows")
))
))]
{
let runtime: Arc<dyn Runtime> = Arc::new(crate::runtime::TokioPerCoreRuntime::new(
resolve_cores(cores_override)?,
)?);
return Ok(Some(RuntimeSelection::from_tokio(runtime)));
}
#[cfg(all(
not(feature = "runtime-tokio"),
not(all(
feature = "serve-prime",
feature = "runtime-prime-reactor",
any(target_os = "linux", target_os = "macos", target_os = "windows")
))
))]
{
let _ = cores_override;
Ok(None)
}
}
#[allow(clippy::needless_return)]
pub fn offline_runtime() -> Result<Arc<dyn Runtime>, ProximaError> {
#[cfg(all(
feature = "runtime-prime-executor",
feature = "runtime-prime-inbox-alloc",
feature = "runtime-prime-reactor",
feature = "runtime-prime-bgpool"
))]
{
return Ok(Arc::new(crate::runtime::PrimeRuntime::new(1)?));
}
#[cfg(all(
feature = "runtime-tokio",
not(all(
feature = "runtime-prime-executor",
feature = "runtime-prime-inbox-alloc",
feature = "runtime-prime-reactor",
feature = "runtime-prime-bgpool"
))
))]
{
return Ok(Arc::new(crate::runtime::TokioPerCoreRuntime::new(1)?));
}
#[cfg(all(
not(feature = "runtime-tokio"),
not(all(
feature = "runtime-prime-executor",
feature = "runtime-prime-inbox-alloc",
feature = "runtime-prime-reactor",
feature = "runtime-prime-bgpool"
))
))]
{
Err(ProximaError::Config(
"offline_runtime: no runtime backend linked (enable runtime-prime or runtime-tokio)"
.into(),
))
}
}
#[cfg(feature = "any-listener")]
fn new_any_registry() -> Result<Arc<proxima_listen::any::AnyRegistry>, ProximaError> {
let any_registry = Arc::new(proxima_listen::any::AnyRegistry::new());
any_registry.register(Arc::new(crate::listeners::H1AnyProtocol::new()))?;
#[cfg(feature = "http2")]
any_registry.register(Arc::new(
crate::listeners::H2PriorKnowledgeAnyProtocol::new(),
))?;
Ok(any_registry)
}
impl App {
pub fn new() -> Result<Self, ProximaError> {
let listen_registry = Arc::new(ListenRegistry::new());
#[cfg(any(feature = "http1", feature = "http1-native"))]
listen_registry.register(Arc::new(HttpListenProtocol::new()))?;
#[cfg(feature = "tokio")]
listen_registry.register(Arc::new(McpListenProtocol::new()))?;
#[cfg(feature = "tcp")]
listen_registry.register(Arc::new(crate::listeners::StreamListenProtocol::new()))?;
#[cfg(feature = "any-listener")]
let any_registry = new_any_registry()?;
#[cfg(feature = "any-listener")]
let any_default_handlers = Arc::new(ArcSwap::from_pointee(BTreeMap::new()));
let (
runtime,
acceptor_factory,
datagram_factory,
unix_upstream_factory,
packet_listener_factory,
) = split_runtime_selection(resolve_runtime_selection(None, None)?);
let load_context = LoadContext::with_default_registry()?;
#[cfg(feature = "recording")]
if let Some(rt) = &runtime {
let _ = load_context.recording_spigot.set(rt.clone());
load_context
.recording_source_registry
.set_runtime(rt.clone());
}
Ok(Self {
pipes: BTreeMap::new(),
pipe_specs: BTreeMap::new(),
sources: BTreeMap::new(),
router: Arc::new(ArcSwap::from_pointee(Router::new())),
load_context,
listen_registry,
runtime,
acceptor_factory,
datagram_factory,
unix_upstream_factory,
packet_listener_factory,
#[cfg(feature = "any-listener")]
any_registry,
#[cfg(feature = "any-listener")]
any_default_handlers,
})
}
#[must_use]
pub fn with_runtime(mut self, runtime: Arc<dyn crate::runtime::Runtime>) -> Self {
self.runtime = Some(runtime);
self
}
#[must_use]
pub fn runtime(&self) -> Option<Arc<dyn crate::runtime::Runtime>> {
self.runtime.clone()
}
#[must_use]
pub fn acceptor_factory(&self) -> Option<Arc<dyn AcceptorFactory>> {
self.acceptor_factory.clone()
}
#[must_use]
pub fn with_acceptor_factory(mut self, factory: Arc<dyn AcceptorFactory>) -> Self {
self.acceptor_factory = Some(factory);
self
}
#[must_use]
pub fn datagram_factory(&self) -> Option<Arc<dyn DatagramFactory>> {
self.datagram_factory.clone()
}
#[must_use]
pub fn with_datagram_factory(mut self, factory: Arc<dyn DatagramFactory>) -> Self {
self.datagram_factory = Some(factory);
self
}
#[must_use]
pub fn unix_upstream_factory(&self) -> Option<Arc<dyn UnixUpstreamFactory>> {
self.unix_upstream_factory.clone()
}
#[must_use]
pub fn packet_listener_factory(
&self,
) -> Option<Arc<dyn proxima_net::packet::PacketListenerFactory>> {
self.packet_listener_factory.clone()
}
#[must_use]
pub fn with_runtime_selection(mut self, selection: crate::runtime::RuntimeSelection) -> Self {
crate::runtime::install_runtime(selection.clone());
self.runtime = Some(selection.runtime);
self.acceptor_factory = Some(selection.acceptor_factory);
self.datagram_factory = selection.datagram_factory;
self.unix_upstream_factory = selection.unix_upstream_factory;
self.packet_listener_factory = selection.packet_listener_factory;
self
}
pub fn pipe_builder(&mut self, name: impl Into<String>) -> AppPipeBuilder<'_> {
AppPipeBuilder {
app: self,
name: name.into(),
spec: None,
mount: None,
methods: None,
}
}
#[must_use]
pub fn pipe_builder_from_existing(&mut self, name: &str) -> Option<AppPipeBuilder<'_>> {
let existing_spec = self.pipe_specs.get(name).cloned()?;
Some(AppPipeBuilder {
app: self,
name: name.to_string(),
spec: Some(Spec::Inline(existing_spec)),
mount: None,
methods: None,
})
}
pub async fn pipe(
&mut self,
name: impl Into<String>,
spec: impl Into<Spec>,
) -> Result<PipeHandle, ProximaError> {
let name = name.into();
let mut resolved = spec.into().resolve(&self.load_context.config_formats)?;
if let Some(value) = resolved.as_value_mut()
&& let Value::Object(table) = value
&& !table.contains_key("name")
{
table.insert("name".into(), Value::String(name.clone()));
}
let (handle, spec_for_storage) = match resolved {
ResolvedSpec::Handle(handle) => (handle, None),
ResolvedSpec::Value(value) => {
let handle = load(value.clone(), &self.load_context).await?;
(handle, Some(value))
}
};
if let Some(spec_value) = spec_for_storage {
self.pipe_specs.insert(name.clone(), spec_value);
} else {
self.pipe_specs.remove(&name);
}
self.pipes.insert(name, handle.clone());
Ok(handle)
}
#[must_use]
pub fn lookup_pipe_spec(&self, name: &str) -> Option<&Value> {
self.pipe_specs.get(name)
}
pub fn pipes(&self) -> &BTreeMap<String, PipeHandle> {
&self.pipes
}
pub fn lookup_pipe(&self, name: &str) -> Option<PipeHandle> {
self.pipes.get(name).cloned()
}
pub fn source(
&mut self,
name: impl Into<String>,
source: impl Into<proxima_primitives::pipe::SourceHandle>,
) {
self.sources.insert(name.into(), source.into());
}
pub fn sources(&self) -> impl Iterator<Item = &str> {
self.sources.keys().map(String::as_str)
}
#[must_use]
pub fn lookup_source(&self, name: &str) -> Option<proxima_primitives::pipe::SourceHandle> {
self.sources.get(name).cloned()
}
pub async fn update_pipe(
&mut self,
name: &str,
spec: impl Into<Spec>,
) -> Result<PipeHandle, ProximaError> {
let old_handle = self.pipes.get(name).cloned().ok_or_else(|| {
ProximaError::NotFound(format!(
"no pipe registered as '{name}'; use App::pipe to register first"
))
})?;
let mut resolved = spec.into().resolve(&self.load_context.config_formats)?;
if let Some(value) = resolved.as_value_mut()
&& let Value::Object(table) = value
&& !table.contains_key("name")
{
table.insert("name".into(), Value::String(name.to_string()));
}
let (handle, spec_for_storage) = match resolved {
ResolvedSpec::Handle(handle) => (handle, None),
ResolvedSpec::Value(value) => {
let handle = load(value.clone(), &self.load_context).await?;
(handle, Some(value))
}
};
match spec_for_storage {
Some(spec_value) => {
self.pipe_specs.insert(name.to_string(), spec_value);
}
None => {
self.pipe_specs.remove(name);
}
}
self.pipes.insert(name.to_string(), handle.clone());
let snapshot = self.router.load();
let existing = snapshot.mounts();
let mut new_router = Router::with_capacity(existing.len());
for mount in existing {
let pipe = if Arc::ptr_eq(&mount.pipe, &old_handle) {
handle.clone()
} else {
mount.pipe.clone()
};
new_router.add(Mount {
path: mount.path.clone(),
pipe,
methods: mount.methods.clone(),
host: mount.host.clone(),
label: Arc::clone(&mount.label),
pipe_name: Arc::clone(&mount.pipe_name),
});
}
drop(snapshot);
self.router.store(Arc::new(new_router));
Ok(handle)
}
pub fn remove_pipe(&mut self, name: &str) -> Result<(), ProximaError> {
let old_handle = self
.pipes
.remove(name)
.ok_or_else(|| ProximaError::NotFound(format!("no pipe registered as '{name}'")))?;
let mut router = (*self.router.load_full()).clone();
let labels: Vec<Arc<[u8]>> = router
.mounts()
.iter()
.filter(|mount| Arc::ptr_eq(&mount.pipe, &old_handle))
.map(|mount| Arc::clone(&mount.label))
.collect();
for label in labels {
let label_str = std::str::from_utf8(&label).unwrap_or("");
router.remove(label_str);
}
self.router.store(Arc::new(router));
Ok(())
}
pub fn unmount(&self, path: &str) -> bool {
let mut router = (*self.router.load_full()).clone();
let removed = router.remove(path);
if removed > 0 {
self.router.store(Arc::new(router));
}
removed > 0
}
pub fn mount<Target, Via>(&self, path: &str, target: Target) -> Result<(), ProximaError>
where
Target: IntoMountTarget<Via>,
{
let target = target.into_mount_target();
let mount = match target {
MountTarget::Handle(handle) => Mount::new(path, handle),
MountTarget::Named(name) => {
let pipe = self.lookup_pipe(&name).ok_or_else(|| {
ProximaError::NotFound(format!("no pipe registered as '{name}'"))
})?;
Mount::new(path, pipe).named(name.clone())
}
};
let mut router = (*self.router.load_full()).clone();
router.add(mount);
self.router.store(Arc::new(router));
Ok(())
}
pub fn mount_with_methods<Target, Via>(
&self,
path: &str,
target: Target,
methods: MethodFilter,
) -> Result<(), ProximaError>
where
Target: IntoMountTarget<Via>,
{
let target = target.into_mount_target();
let mount = match target {
MountTarget::Handle(handle) => Mount::new(path, handle).with_methods(methods),
MountTarget::Named(name) => {
let pipe = self.lookup_pipe(&name).ok_or_else(|| {
ProximaError::NotFound(format!("no pipe registered as '{name}'"))
})?;
Mount::new(path, pipe)
.with_methods(methods)
.named(name.clone())
}
};
let mut router = (*self.router.load_full()).clone();
router.add(mount);
self.router.store(Arc::new(router));
Ok(())
}
pub fn mount_full<Target, Via>(
&self,
path: &str,
target: Target,
methods: MethodFilter,
host: crate::mount::HostFilter,
) -> Result<(), ProximaError>
where
Target: IntoMountTarget<Via>,
{
let target = target.into_mount_target();
let mount = match target {
MountTarget::Handle(handle) => Mount::new(path, handle)
.with_methods(methods)
.with_host(host),
MountTarget::Named(name) => {
let pipe = self.lookup_pipe(&name).ok_or_else(|| {
ProximaError::NotFound(format!("no pipe registered as '{name}'"))
})?;
Mount::new(path, pipe)
.with_methods(methods)
.with_host(host)
.named(name.clone())
}
};
let mut router = (*self.router.load_full()).clone();
router.add(mount);
self.router.store(Arc::new(router));
Ok(())
}
#[doc(hidden)]
pub fn __internal_assemble(
load_context: LoadContext,
listen_registry: Arc<ListenRegistry>,
router: Arc<ArcSwap<Router>>,
cores_override: Option<usize>,
runtime_selection: Option<RuntimeSelection>,
) -> Result<Self, ProximaError> {
let had_explicit_selection = runtime_selection.is_some();
let resolved = resolve_runtime_selection(runtime_selection, cores_override)?;
if had_explicit_selection && let Some(selection) = &resolved {
crate::runtime::install_runtime(selection.clone());
}
let (
runtime,
acceptor_factory,
datagram_factory,
unix_upstream_factory,
packet_listener_factory,
) = split_runtime_selection(resolved);
#[cfg(feature = "recording")]
if let Some(rt) = &runtime {
let _ = load_context.recording_spigot.set(rt.clone());
load_context
.recording_source_registry
.set_runtime(rt.clone());
}
Ok(Self {
pipes: BTreeMap::new(),
pipe_specs: BTreeMap::new(),
sources: BTreeMap::new(),
router,
load_context,
listen_registry,
runtime,
acceptor_factory,
datagram_factory,
unix_upstream_factory,
packet_listener_factory,
#[cfg(feature = "any-listener")]
any_registry: new_any_registry()?,
#[cfg(feature = "any-listener")]
any_default_handlers: Arc::new(ArcSwap::from_pointee(BTreeMap::new())),
})
}
#[must_use]
pub fn load_context(&self) -> &LoadContext {
&self.load_context
}
pub fn router_handle(&self) -> PipeHandle {
into_handle(RouterDispatch {
router: self.router.clone(),
})
}
#[must_use]
pub fn metrics(&self) -> Option<Arc<Metrics>> {
self.load_context.metrics.clone()
}
#[must_use]
pub fn telemetry(&self) -> TelemetryHandle {
self.load_context.telemetry.clone()
}
#[must_use]
pub fn builder() -> crate::app_builder::AppBuilder {
crate::app_builder::AppBuilder::new()
}
pub async fn run_until_signal(&self, config: RunConfig) -> Result<Shutdown, ProximaError> {
let protocol = self.listen_registry.get(&config.protocol)?;
let dispatch: PipeHandle = into_handle(ContextInjector::new(
self.router_handle(),
self.load_context.telemetry.clone(),
));
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let bind = config.bind;
let spec = config.spec;
let telemetry = self.load_context.telemetry.clone();
let runtime = self.runtime.clone().ok_or_else(|| {
ProximaError::Config(
"App has no Runtime installed: enable the `runtime-tokio` \
feature (default) or call `.with_runtime(...)` before \
`run_until_signal`. Pipe::call returns ?Send and needs a \
per-core executor"
.into(),
)
})?;
#[cfg(feature = "recording")]
{
let _ = self.load_context.recording_spigot.set(runtime.clone());
self.load_context
.recording_source_registry
.set_runtime(runtime.clone());
}
let runtime_for_factory = runtime.clone();
let acceptor_factory = self.acceptor_factory.clone();
let datagram_factory = self.datagram_factory.clone();
let drain_notify = std::sync::Arc::new(proxima_primitives::sync::Notify::new());
let drain_notify_for_factory = drain_notify.clone();
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), ProximaError>>();
let ready_tx_for_outcome = ready_tx.clone();
runtime
.spawn_factory_on_core(
crate::runtime::CoreId(0),
Box::new(move || {
let mut context = proxima_listen::ServeContext::new(telemetry)
.with_runtime(runtime_for_factory)
.with_ready_signal(ready_tx);
if let Some(factory) = acceptor_factory {
context = context.with_acceptor_factory(factory);
}
if let Some(factory) = datagram_factory {
context = context.with_datagram_factory(factory);
}
Box::pin(async move {
if let Err(error) = protocol
.serve(bind, dispatch, &spec, context, shutdown_rx)
.await
{
warn!(?error, %bind, "listener exited with error");
let _ = ready_tx_for_outcome.send(Err(error));
}
drain_notify_for_factory.notify_waiters();
})
}),
)
.map_err(|err| {
crate::error::ProximaError::Config(format!(
"listener spawn on core 0 failed: {err}"
))
})?;
match ready_rx.recv_timeout(proxima_listen::handle::LISTENER_READY_TIMEOUT) {
Ok(Ok(())) => {}
Ok(Err(error)) => return Err(error),
Err(_) => {
return Err(crate::error::ProximaError::Config(format!(
"listener did not become ready within {:?}",
proxima_listen::handle::LISTENER_READY_TIMEOUT
)));
}
}
let mut source_lifecycle = proxima_primitives::pipe::ProducerLifecycle::new();
let source_cancel = source_lifecycle.cancel_signal();
for (name, source) in &self.sources {
source_lifecycle.spawn_from_source(name, source);
}
Ok(Shutdown {
shutdown_tx: Some(shutdown_tx),
drain_notify,
_runtime: self.runtime.clone(),
source_lifecycle: Some(source_lifecycle),
source_cancel,
})
}
pub async fn serve(
&self,
config: impl Into<RunConfig>,
) -> Result<crate::server::Server, ProximaError> {
let shutdown = self.run_until_signal(config.into()).await?;
let control: crate::control_plane::DynControlPlane =
Arc::new(crate::control_plane::StaticControlPlane::new(Vec::new()));
Ok(crate::server::Server::new(shutdown, control))
}
pub async fn apply_settings(
&mut self,
settings: &crate::settings::ProximaSettings,
) -> Result<(), ProximaError> {
for (name, entry) in &settings.upstreams {
let value = registry_entry_value(entry);
self.pipe(name.clone(), Spec::Inline(value)).await?;
}
for (name, entry) in &settings.pipes {
let composed = compose_pipe_spec(entry, settings)?;
self.pipe(name.clone(), Spec::Inline(composed)).await?;
}
for (name, entry) in &settings.producers {
let value = registry_entry_value(entry);
let type_key = entry.r#type.as_str();
let factory = self.load_context.source_registry.get(type_key)?;
let source = factory.build(&value)?;
self.source(name.clone(), source);
}
Ok(())
}
pub fn register_listen_protocol(
&self,
protocol: Arc<dyn proxima_listen::ListenProtocol>,
) -> Result<(), ProximaError> {
self.listen_registry.register(protocol)
}
#[cfg(feature = "any-listener")]
#[must_use]
pub fn any_registry(&self) -> Arc<proxima_listen::any::AnyRegistry> {
self.any_registry.clone()
}
#[cfg(feature = "any-listener")]
pub fn register_any_default_handler(
&self,
name: impl Into<String>,
handler: proxima_listen::any::AnyHandler,
) {
let name = name.into();
loop {
let current = self.any_default_handlers.load_full();
let mut next: BTreeMap<String, proxima_listen::any::AnyHandler> = (*current).clone();
next.insert(name.clone(), handler.clone());
let prev = self
.any_default_handlers
.compare_and_swap(¤t, Arc::new(next));
if Arc::ptr_eq(&prev, ¤t) {
return;
}
}
}
#[cfg(feature = "any-listener")]
#[must_use]
pub fn any_default_handlers(&self) -> Arc<BTreeMap<String, proxima_listen::any::AnyHandler>> {
self.any_default_handlers.load_full()
}
pub fn build_listener(&self, spec: ListenerSpec) -> Result<ListenerHandle, ProximaError> {
let dispatch: PipeHandle = into_handle(ContextInjector::new(
self.router_handle(),
self.load_context.telemetry.clone(),
));
spec.attach(dispatch).run_with_runtime(
&self.listen_registry,
self.load_context.telemetry.clone(),
self.runtime.clone(),
self.acceptor_factory.clone(),
self.datagram_factory.clone(),
)
}
pub async fn load_full(
&mut self,
spec: impl Into<Spec>,
) -> Result<Vec<ListenerHandle>, ProximaError> {
let resolved = spec.into().resolve(&self.load_context.config_formats)?;
let value = match resolved {
ResolvedSpec::Value(value) => value,
ResolvedSpec::Handle(_) => {
return Err(ProximaError::Config(
"load_full requires a Value spec, not a pre-built Handle".into(),
));
}
};
let table = value.as_object().ok_or_else(|| {
ProximaError::Config("load_full expects a top-level table / object".into())
})?;
if let Some(schema_blocks) = table.get("schema").and_then(Value::as_array) {
for entry in schema_blocks {
let name = entry
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| ProximaError::Config("[[schema]] requires `name`".into()))?
.to_string();
let schema_value = entry.get("schema").cloned().ok_or_else(|| {
ProximaError::Config(format!(
"[[schema]] `{name}` requires a nested `schema = {{ type = \"...\", ... }}` block"
))
})?;
let schema: crate::schema::Schema =
serde_json::from_value(schema_value).map_err(|err| {
ProximaError::Config(format!("[[schema]] `{name}` decode: {err}"))
})?;
self.load_context.schemas.register(name, schema)?;
}
}
if let Some(pipes) = table.get("pipe").and_then(Value::as_array) {
for entry in pipes {
let name = entry
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| ProximaError::Config("[[pipe]] requires `name`".into()))?
.to_string();
let mut spec_value = entry.clone();
if let Some(obj) = spec_value.as_object_mut() {
obj.remove("name");
obj.insert("name".into(), Value::String(name.clone()));
}
let handle = load(spec_value, &self.load_context).await?;
self.pipes.insert(name, handle);
}
}
let mut handles = Vec::new();
if let Some(listens) = table.get("listen").and_then(Value::as_array) {
for listen in listens {
let handle = self.bind_listener(listen)?;
handles.push(handle);
}
} else {
return Err(ProximaError::Config(
"load_full found no `[[listen]]` blocks; use App::pipe / App::mount / App::run_until_signal for single-listener configs".into(),
));
}
Ok(handles)
}
fn bind_listener(&self, listen: &Value) -> Result<ListenerHandle, ProximaError> {
let protocol_name = listen
.get("type")
.and_then(Value::as_str)
.unwrap_or("http")
.to_string();
let bind_str = listen
.get("bind")
.and_then(Value::as_str)
.ok_or_else(|| ProximaError::Config("[[listen]] requires `bind`".into()))?;
let bind: SocketAddr = bind_str
.parse()
.map_err(|err| ProximaError::Config(format!("invalid bind '{bind_str}': {err}")))?;
let router = self.build_listener_router(&protocol_name, listen)?;
let dispatch: PipeHandle = into_handle(ContextInjector::new(
into_handle(RouterDispatch {
router: Arc::new(ArcSwap::from_pointee(router)),
}),
self.load_context.telemetry.clone(),
));
let listener_spec = ListenerSpec {
bind,
protocol_name,
shutdown: proxima_listen::handle::ShutdownPolicy::drain_30s(),
spec: listen.clone(),
protocol: None,
};
listener_spec.attach(dispatch).run_with_runtime(
&self.listen_registry,
self.load_context.telemetry.clone(),
self.runtime.clone(),
self.acceptor_factory.clone(),
self.datagram_factory.clone(),
)
}
fn build_listener_router(
&self,
protocol_name: &str,
listen: &Value,
) -> Result<Router, ProximaError> {
let mut router = Router::new();
match protocol_name {
"http" => {
let mounts = match listen.get("mount") {
Some(Value::Array(arr)) => arr.clone(),
Some(_) => {
return Err(ProximaError::Config(
"[[listen.mount]] must be an array".into(),
));
}
None => {
if let Some(name) = listen.get("pipe").and_then(Value::as_str) {
let target = self.lookup_pipe(name).ok_or_else(|| {
ProximaError::NotFound(format!(
"[[listen]] references unknown pipe '{name}'"
))
})?;
router.add(Mount::new("/{*path}", target));
}
return Ok(router);
}
};
for mount in &mounts {
let path = mount.get("path").and_then(Value::as_str).ok_or_else(|| {
ProximaError::Config("[[listen.mount]] requires `path`".into())
})?;
let pipe_name = mount.get("pipe").and_then(Value::as_str).ok_or_else(|| {
ProximaError::Config("[[listen.mount]] requires `pipe`".into())
})?;
let target = self.lookup_pipe(pipe_name).ok_or_else(|| {
ProximaError::NotFound(format!(
"[[listen.mount]] references unknown pipe '{pipe_name}'"
))
})?;
let methods = mount
.get("methods")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect::<Vec<_>>()
})
.map(MethodFilter::only)
.unwrap_or_else(MethodFilter::any);
let host = mount
.get("host")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect::<Vec<_>>()
})
.map(crate::mount::HostFilter::only)
.unwrap_or_else(crate::mount::HostFilter::any);
router.add(
Mount::new(path, target)
.with_methods(methods)
.with_host(host),
);
}
}
_ => {
let pipe_name = listen.get("pipe").and_then(Value::as_str).ok_or_else(|| {
ProximaError::Config(format!(
"[[listen]] type='{protocol_name}' requires `pipe`"
))
})?;
let target = self.lookup_pipe(pipe_name).ok_or_else(|| {
ProximaError::NotFound(format!(
"[[listen]] references unknown pipe '{pipe_name}'"
))
})?;
router.add(Mount::new("/{*path}", target));
}
}
Ok(router)
}
}
#[must_use = "AppPipeBuilder is inert until .build().await is called"]
pub struct AppPipeBuilder<'app> {
app: &'app mut App,
name: String,
spec: Option<Spec>,
mount: Option<String>,
methods: Option<crate::mount::MethodFilter>,
}
impl<'app> AppPipeBuilder<'app> {
pub fn dispatch(mut self, spec: impl Into<Spec>) -> Self {
self.spec = Some(spec.into());
self
}
pub fn mount(mut self, path: impl Into<String>) -> Self {
self.mount = Some(path.into());
self
}
pub fn methods(mut self, filter: crate::mount::MethodFilter) -> Self {
self.methods = Some(filter);
self
}
pub async fn build(self) -> Result<PipeHandle, ProximaError> {
let Self {
app,
name,
spec,
mount,
methods,
} = self;
let spec = spec.ok_or_else(|| {
ProximaError::Config(format!(
"AppPipeBuilder('{name}'): dispatch(...) is required before build()"
))
})?;
let handle = if app.pipes.contains_key(&name) {
app.update_pipe(&name, spec).await?
} else {
app.pipe(name.clone(), spec).await?
};
if let Some(path) = mount {
let target = MountTarget::Named(name);
match methods {
Some(filter) => app.mount_with_methods(&path, target, filter)?,
None => app.mount(&path, target)?,
}
}
Ok(handle)
}
}
pub enum MountTarget {
Handle(PipeHandle),
Named(String),
}
pub struct ViaName;
pub struct ViaPipe;
pub struct ViaFn;
pub struct ViaTarget;
#[diagnostic::on_unimplemented(
message = "`{Self}` can't be mounted: expected a request handler — an `async fn(Request<Bytes>) -> Result<Response<Bytes>, ProximaError>`, a handler-shaped pipe, or a registered pipe name",
label = "not something `App::mount` can dispatch to"
)]
pub trait IntoMountTarget<Via> {
fn into_mount_target(self) -> MountTarget;
}
impl IntoMountTarget<ViaName> for &str {
fn into_mount_target(self) -> MountTarget {
MountTarget::Named(self.to_string())
}
}
impl IntoMountTarget<ViaName> for String {
fn into_mount_target(self) -> MountTarget {
MountTarget::Named(self)
}
}
impl IntoMountTarget<ViaTarget> for MountTarget {
fn into_mount_target(self) -> MountTarget {
self
}
}
impl<Implementor> IntoMountTarget<ViaPipe> for Implementor
where
Implementor: Handler + 'static,
{
fn into_mount_target(self) -> MountTarget {
MountTarget::Handle(into_handle(self))
}
}
struct FnHandler<Implementor>(Implementor);
impl<Implementor, Fut> SendPipe for FnHandler<Implementor>
where
Implementor: Fn(Request<Bytes>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response<Bytes>, ProximaError>> + Send + 'static,
{
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
(self.0)(request)
}
}
impl<Implementor, Fut> IntoMountTarget<ViaFn> for Implementor
where
Implementor: Fn(Request<Bytes>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Response<Bytes>, ProximaError>> + Send + 'static,
{
fn into_mount_target(self) -> MountTarget {
MountTarget::Handle(into_handle(FnHandler(self)))
}
}
const SOURCE_SHUTDOWN_GRACE: std::time::Duration = std::time::Duration::from_secs(5);
pub struct Shutdown {
shutdown_tx: Option<oneshot::Sender<()>>,
drain_notify: Arc<proxima_primitives::sync::Notify>,
_runtime: Option<Arc<dyn crate::runtime::Runtime>>,
source_lifecycle: Option<proxima_primitives::pipe::ProducerLifecycle>,
source_cancel: proxima_core::signal::Signal,
}
impl Shutdown {
pub fn stop(mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
self.source_cancel.fire();
}
#[cfg(test)]
pub(crate) fn for_test_with_tx(tx: oneshot::Sender<()>) -> Self {
Self {
shutdown_tx: Some(tx),
drain_notify: Arc::new(proxima_primitives::sync::Notify::new()),
_runtime: None,
source_lifecycle: None,
source_cancel: proxima_core::signal::Signal::new(),
}
}
#[cfg(test)]
pub(crate) fn for_test(_rx: oneshot::Receiver<()>) -> Self {
let (tx, _rx_unused) = oneshot::channel();
Self {
shutdown_tx: Some(tx),
drain_notify: Arc::new(proxima_primitives::sync::Notify::new()),
_runtime: None,
source_lifecycle: None,
source_cancel: proxima_core::signal::Signal::new(),
}
}
pub async fn drain(mut self) -> crate::shutdown::ShutdownReport {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
self.drain_notify.notified().await;
let mut report = match self._runtime.take() {
Some(runtime) => {
let barrier = crate::shutdown::ShutdownBarrier::new(runtime);
barrier.broadcast_drop().await
}
None => crate::shutdown::ShutdownReport {
cores_acked: 0,
hooks_drained: 0,
sources_drained: 0,
sources_aborted: 0,
},
};
if let Some(source_lifecycle) = self.source_lifecycle.take() {
let source_report = source_lifecycle.shutdown(SOURCE_SHUTDOWN_GRACE).await;
report.sources_drained = source_report.drained;
report.sources_aborted = source_report.aborted;
}
report
}
}
impl Drop for Shutdown {
fn drop(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
self.source_cancel.fire();
}
}
fn registry_entry_value(entry: &crate::settings::RegistryEntry) -> Value {
let mut map = match &entry.spec {
Value::Object(m) => m.clone(),
other => {
let mut m = serde_json::Map::new();
m.insert("value".into(), other.clone());
m
}
};
map.insert("type".into(), Value::String(entry.r#type.clone()));
Value::Object(map)
}
fn compose_pipe_spec(
entry: &crate::settings::RegistryEntry,
settings: &crate::settings::ProximaSettings,
) -> Result<Value, ProximaError> {
let mut map = match &entry.spec {
Value::Object(m) => m.clone(),
_ => {
return Err(ProximaError::Config(
"pipe entry spec must be an object with `mount` + `chain`".into(),
));
}
};
let chain_names: Vec<String> = map
.get("chain")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
if chain_names.is_empty() {
map.insert("type".into(), Value::String(entry.r#type.clone()));
return Ok(Value::Object(map));
}
let (leaf_name, mw_names) = chain_names
.split_last()
.ok_or_else(|| ProximaError::Config("pipe chain must not be empty".into()))?;
let leaf_entry = settings.upstreams.get(leaf_name).ok_or_else(|| {
ProximaError::Config(format!(
"pipe chain leaf `{leaf_name}` not found in [upstream.*]"
))
})?;
let mut leaf_map = match &leaf_entry.spec {
Value::Object(m) => m.clone(),
_ => serde_json::Map::new(),
};
leaf_map.insert("type".into(), Value::String(leaf_entry.r#type.clone()));
let mw_array: Vec<Value> = mw_names
.iter()
.map(|name| {
let mw_entry = settings.middlewares.get(name).ok_or_else(|| {
ProximaError::Config(format!(
"pipe chain middleware `{name}` not found in [middleware.*]"
))
})?;
Ok(registry_entry_value(mw_entry))
})
.collect::<Result<_, ProximaError>>()?;
if !mw_array.is_empty() {
leaf_map.insert("middleware".into(), Value::Array(mw_array));
}
map.remove("chain");
for (k, v) in &map {
leaf_map.entry(k.clone()).or_insert(v.clone());
}
Ok(Value::Object(leaf_map))
}
#[derive(Clone)]
pub struct RunConfig {
pub bind: SocketAddr,
pub protocol: String,
pub spec: Value,
}
impl RunConfig {
#[must_use]
pub fn http(bind: SocketAddr) -> Self {
Self {
bind,
protocol: "http".into(),
spec: Value::Null,
}
}
}
struct RouterDispatch {
router: Arc<ArcSwap<Router>>,
}
impl SendPipe for RouterDispatch {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
mut request: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> {
let router = self.router.load_full();
async move {
let routed = router
.route_with_params(&request)
.map(|(mount, params)| (mount.clone(), params));
let Some((mount, params)) = routed else {
let telemetry = request.context.telemetry.clone();
let labels = crate::telemetry::Labels::from_pairs(&[
("pipe", "__unrouted__"),
("status_class", "4xx"),
]);
telemetry.counter_inc("proxima.requests_total", &labels, 1);
return Ok(Response::not_found().with_body("no route matched"));
};
request.context.path_params = params;
if request.context.pipe_label.is_none() {
request.context.pipe_label = Some(Arc::from(mount.pipe_name.as_bytes()));
}
let started = std::time::Instant::now();
let telemetry = request.context.telemetry.clone();
let labels = request.context.metric_labels(&[]);
telemetry.counter_inc("proxima.requests_total", &labels, 1);
let outcome = SendPipe::call(&mount.pipe, request).await;
let elapsed_ms = started.elapsed().as_secs_f64() * 1_000.0;
telemetry.histogram_record("proxima.request.latency_ms", &labels, elapsed_ms);
outcome
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::pin::Pin;
use super::*;
use serde_json::json;
fn build_request(method: &str, path: &str) -> Request<Bytes> {
Request::builder()
.method(method)
.path(path)
.build()
.expect("builder")
}
#[proxima::test]
async fn registered_pipe_can_be_mounted_by_name() {
let mut app = App::new().expect("app");
app.pipe(
"cache",
json!({"kv": "cache", "ttl": "1h", "max_entries": 100}),
)
.await
.expect("register");
app.mount("/users/{id}", "cache").expect("mount");
let dispatch = app.router_handle();
let outcome = SendPipe::call(&dispatch, build_request("GET", "/users/42")).await;
assert!(
matches!(outcome, Err(ProximaError::NoData)),
"kv miss propagates as NoData"
);
}
#[proxima::test]
async fn unmatched_path_returns_404() {
let mut app = App::new().expect("app");
app.pipe("cache", json!({"kv": "cache", "max_entries": 100}))
.await
.expect("register");
app.mount("/foo", "cache").expect("mount");
let dispatch = app.router_handle();
let response = SendPipe::call(&dispatch, build_request("GET", "/bar"))
.await
.expect("dispatch");
assert_eq!(response.status, 404);
}
#[proxima::test]
async fn mount_by_unknown_name_returns_not_found() {
let app = App::new().expect("app");
let outcome = app.mount("/foo", "missing");
assert!(matches!(outcome, Err(ProximaError::NotFound(_))));
}
struct Echo;
impl SendPipe for Echo {
type In = Request<Bytes>;
type Out = Response<Bytes>;
type Err = ProximaError;
fn call(
&self,
request: Request<Bytes>,
) -> impl std::future::Future<Output = Result<Response<Bytes>, ProximaError>> + Send
{
async move {
let (_, body) = request.body_bytes().await?;
Ok(Response::ok(body))
}
}
}
#[proxima::test]
async fn mount_erases_and_mounts_a_handler_shaped_pipe_in_one_call() {
let app = App::new().expect("app");
app.mount("/echo", Echo).expect("mount");
let dispatch = app.router_handle();
let request = Request::builder()
.method("POST")
.path("/echo")
.body(Bytes::from_static(b"hello"))
.build()
.expect("builder");
let response = SendPipe::call(&dispatch, request).await.expect("dispatch");
assert_eq!(response.status, 200);
let body = response.collect_body().await.expect("body");
assert_eq!(&body[..], b"hello", "mount reaches the erased handler");
}
#[proxima::test]
async fn mount_with_methods_restricts_a_handler_shaped_pipe() {
let app = App::new().expect("app");
app.mount_with_methods("/echo", Echo, MethodFilter::only(["POST".to_string()]))
.expect("mount_with_methods");
let dispatch = app.router_handle();
let response = SendPipe::call(&dispatch, build_request("GET", "/echo"))
.await
.expect("dispatch");
assert_eq!(
response.status, 404,
"GET is outside the mounted method filter"
);
}
async fn echo_fn(request: Request<Bytes>) -> Result<Response<Bytes>, ProximaError> {
let (_, body) = request.body_bytes().await?;
Ok(Response::ok(body))
}
#[proxima::test]
async fn mount_erases_and_mounts_a_bare_async_fn() {
let app = App::new().expect("app");
app.mount("/echo-fn", echo_fn).expect("mount");
let dispatch = app.router_handle();
let request = Request::builder()
.method("POST")
.path("/echo-fn")
.body(Bytes::from_static(b"world"))
.build()
.expect("builder");
let response = SendPipe::call(&dispatch, request).await.expect("dispatch");
assert_eq!(response.status, 200);
let body = response.collect_body().await.expect("body");
assert_eq!(&body[..], b"world", "mount reaches the bare fn via ViaFn");
}
#[proxima::test]
async fn pipe_builder_registers_and_mounts_in_one_chain() {
let mut app = App::new().expect("app");
let handle = app
.pipe_builder("cache")
.dispatch(json!({"kv": "cache", "max_entries": 100}))
.mount("/u/{id}")
.build()
.await
.expect("build");
let _ = &handle;
assert!(app.lookup_pipe("cache").is_some());
let dispatch = app.router_handle();
let outcome = SendPipe::call(&dispatch, build_request("GET", "/u/42")).await;
assert!(
matches!(outcome, Err(ProximaError::NoData)),
"mount-by-builder reaches the same kv path"
);
}
#[proxima::test]
async fn pipe_builder_dispatch_required() {
let mut app = App::new().expect("app");
let outcome = app.pipe_builder("api").mount("/api").build().await;
let err = match outcome {
Ok(_) => panic!("dispatch missing must error"),
Err(err) => err,
};
let message = format!("{err}");
assert!(message.contains("dispatch(...)"), "got: {message}");
}
#[proxima::test]
async fn pipe_builder_without_mount_registers_only() {
let mut app = App::new().expect("app");
app.pipe_builder("unmounted")
.dispatch(json!({"kv": "cache", "max_entries": 8}))
.build()
.await
.expect("build");
assert!(app.lookup_pipe("unmounted").is_some());
let dispatch = app.router_handle();
let response = SendPipe::call(&dispatch, build_request("GET", "/unmounted"))
.await
.expect("dispatch");
assert_eq!(response.status, 404);
}
#[proxima::test]
async fn lookup_pipe_spec_returns_resolved_value_after_registration() {
let mut app = App::new().expect("app");
app.pipe("svc", json!({"kv": "cache", "max_entries": 8}))
.await
.expect("register");
let spec = app.lookup_pipe_spec("svc").expect("spec stored");
assert_eq!(spec.get("kv").and_then(Value::as_str), Some("cache"));
assert_eq!(
spec.get("name").and_then(Value::as_str),
Some("svc"),
"name field gets injected at register time",
);
}
#[proxima::test]
async fn lookup_pipe_spec_returns_none_for_unregistered_name() {
let app = App::new().expect("app");
assert!(app.lookup_pipe_spec("ghost").is_none());
}
#[proxima::test]
async fn pipe_builder_from_existing_pre_fills_spec_and_routes_through_update() {
let mut app = App::new().expect("app");
let original = app
.pipe("svc", json!({"kv": "cache", "max_entries": 4}))
.await
.expect("register");
let original_pipes_count = app.pipes.len();
let modified = app
.pipe_builder_from_existing("svc")
.expect("svc is registered")
.build()
.await
.expect("rebuild");
assert_eq!(app.pipes.len(), original_pipes_count);
assert!(Arc::ptr_eq(
&modified,
&app.lookup_pipe("svc").expect("still registered"),
));
let _ = original;
}
#[proxima::test]
async fn pipe_builder_from_existing_with_dispatch_swap_replaces_spec() {
let mut app = App::new().expect("app");
app.pipe("svc", json!({"kv": "cache", "max_entries": 4}))
.await
.expect("register");
app.pipe_builder_from_existing("svc")
.expect("registered")
.dispatch(json!({"kv": "cache", "max_entries": 64}))
.build()
.await
.expect("rebuild");
let stored = app.lookup_pipe_spec("svc").expect("spec stored");
assert_eq!(
stored.get("max_entries").and_then(Value::as_u64),
Some(64),
"dispatch(...) on the builder overrode the pre-filled spec",
);
}
#[proxima::test]
async fn pipe_builder_from_existing_returns_none_for_unregistered_name() {
let mut app = App::new().expect("app");
assert!(app.pipe_builder_from_existing("ghost").is_none());
}
#[proxima::test]
async fn pipe_inherits_registration_name_when_spec_omits_it() {
let mut app = App::new().expect("app");
let handle = app
.pipe("echo-cached", json!({"kv": "cache", "max_entries": 10}))
.await
.expect("register");
assert!(app.lookup_pipe("echo-cached").is_some());
let _ = &handle;
}
struct FailingListenProtocol;
impl proxima_listen::ListenProtocol for FailingListenProtocol {
fn name(&self) -> &str {
"failing-listen-protocol"
}
fn serve(
&self,
_bind: SocketAddr,
_dispatch: PipeHandle,
_spec: &Value,
_context: proxima_listen::ServeContext,
_shutdown: oneshot::Receiver<()>,
) -> Pin<Box<dyn Future<Output = Result<(), ProximaError>> + Send + '_>> {
Box::pin(async move {
Err(ProximaError::Config(
"synthetic startup failure: missing required feature `never-enabled`".into(),
))
})
}
}
#[proxima::test]
async fn run_until_signal_surfaces_serve_startup_error_instead_of_timeout() {
let app = App::new().expect("app");
app.register_listen_protocol(Arc::new(FailingListenProtocol))
.expect("register");
let config = RunConfig {
bind: "127.0.0.1:0".parse().expect("addr"),
protocol: "failing-listen-protocol".into(),
spec: Value::Null,
};
let outcome = app.run_until_signal(config).await;
let error = match outcome {
Ok(_) => panic!("a listener whose serve() fails must not report success"),
Err(error) => error,
};
let message = error.to_string();
assert!(
message.contains("synthetic startup failure"),
"caller must see the actual startup cause, not a generic ready-timeout \
message; got: {message}"
);
assert!(
!message.contains("did not become ready"),
"a listener that failed BEFORE ever reporting ready must not present as \
a bare timeout; got: {message}"
);
}
#[cfg(all(
feature = "http1",
any(
feature = "runtime-tokio",
all(
feature = "serve-prime",
feature = "runtime-prime-reactor",
any(target_os = "linux", target_os = "macos", target_os = "windows")
)
)
))]
#[proxima::test]
async fn shutdown_drain_fires_per_core_resource_hooks() {
use std::sync::atomic::{AtomicU64, Ordering};
let app = App::new().expect("app");
let runtime = app.runtime().expect("default runtime installed");
let drops_observed = Arc::new(AtomicU64::new(0));
let cores = runtime.num_cores();
let (registered_tx, mut registered_rx) =
proxima_primitives::sync::mpsc::channel::<()>(cores.max(1));
for core_index in 0..cores {
let drops_for_core = drops_observed.clone();
let registered_on_core = registered_tx.clone();
runtime
.spawn_factory_on_core(
crate::runtime::CoreId(core_index),
Box::new(move || {
Box::pin(async move {
crate::shutdown::register_per_core_resource(
format!("core-{core_index}"),
Box::new(move || {
drops_for_core.fetch_add(1, Ordering::SeqCst);
}),
);
let _ = registered_on_core.send(()).await;
})
}),
)
.expect("test-time spawn must succeed on a fresh runtime");
}
for _ in 0..cores {
registered_rx
.recv()
.await
.expect("every per-core registration must report in");
}
let config = RunConfig::http("127.0.0.1:0".parse().expect("addr"));
let shutdown = app.run_until_signal(config).await.expect("run");
let report = shutdown.drain().await;
let expected = runtime.num_cores();
assert_eq!(report.cores_acked, expected, "every core must ack");
assert_eq!(report.hooks_drained, expected, "every hook must fire");
assert_eq!(
drops_observed.load(Ordering::SeqCst) as usize,
expected,
"destructor instrumentation must observe one drop per core",
);
}
}