use std::collections::HashSet;
use std::num::NonZeroUsize;
use frame_core::component::ComponentId;
use frame_core::event::LifecycleSubscription;
use crate::announcer::Announcer;
use crate::config::FrameConfig;
use crate::doc_binding::DocBinding;
use crate::embedded::EmbeddedLiminal;
use crate::error::HostError;
use crate::runtime::{EventLoggerHandle, HostRuntime};
use crate::serve::{ServeStop, serve};
use crate::spec::AppSpec;
const ANNOUNCER_EVENT_BUFFER: usize = 256;
pub struct Application {
runtime: HostRuntime,
logger: EventLoggerHandle,
liminal: EmbeddedLiminal,
doc_binding: Option<DocBinding>,
announcer: Option<Announcer>,
bus_endpoint: String,
components: HashSet<ComponentId>,
}
impl Application {
pub fn boot(config: &FrameConfig, spec: AppSpec) -> Result<Self, HostError> {
if spec.components.is_empty() {
return Err(HostError::ConfigContract {
detail: "the application spec installs no components: a host with nothing to \
host is a composition error, not an empty stack"
.to_owned(),
});
}
let components: HashSet<ComponentId> = spec
.components
.iter()
.map(|install| install.meta.id)
.collect();
let mut runtime = HostRuntime::new(spec.policy)?;
let announcer_subscription: Option<LifecycleSubscription> = match &config.frame.channel {
Some(_) => {
let capacity = NonZeroUsize::new(ANNOUNCER_EVENT_BUFFER)
.ok_or(HostError::SynchronizationPoisoned)?;
Some(runtime.registry().subscribe(capacity)?)
}
None => None,
};
let logger = runtime.spawn_event_logger(components.clone())?;
if let Err(error) = runtime.install(spec.components) {
runtime.abandon_after_boot_failure();
return Err(error);
}
if let Err(source) = (spec.readiness)(runtime.registry()) {
runtime.abandon_after_boot_failure();
return Err(HostError::Application {
stage: "readiness",
source,
});
}
let liminal = match EmbeddedLiminal::boot(&config.bus) {
Ok(liminal) => liminal,
Err(error) => {
if let Err(teardown) = runtime.shutdown(logger) {
tracing::error!(
%teardown,
"frame-core teardown after bus boot failure also failed"
);
}
return Err(error);
}
};
let bus_endpoint = liminal.websocket_endpoint();
tracing::info!(
endpoint = %bus_endpoint,
"embedded bus component booted; /frame/config.json advertises its real bound WebSocket address as busEndpoint"
);
let doc_binding = match DocBinding::boot(&liminal, config) {
Ok(binding) => binding,
Err(error) => {
teardown_after_boot_failure(None, liminal, runtime, logger, "document-service");
return Err(error);
}
};
let announcer = match connect_announcer(config, &liminal, announcer_subscription) {
Ok(announcer) => announcer,
Err(error) => {
teardown_after_boot_failure(
doc_binding,
liminal,
runtime,
logger,
"announcer connect",
);
return Err(error);
}
};
if let Err(error) = announce_boot_facts(spec.announce, &runtime, announcer.as_ref()) {
teardown_after_boot_failure(doc_binding, liminal, runtime, logger, "fact announcement");
return Err(error);
}
Ok(Self {
runtime,
logger,
liminal,
doc_binding,
announcer,
bus_endpoint,
components,
})
}
pub fn serve(
&self,
config: &FrameConfig,
external_stop: impl Future<Output = ()> + Send + 'static,
) -> Result<ServeStop, HostError> {
if let Some(announcer) = &self.announcer {
announcer.start(self.components.clone())?;
}
let tokio_runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|source| HostError::AsyncRuntime { source })?;
tokio_runtime.block_on(serve(
config,
&self.liminal,
self.bus_endpoint.clone(),
external_stop,
))
}
pub fn shutdown(self) -> Result<(), HostError> {
if let Some(announcer) = &self.announcer {
announcer.close_intake();
}
let doc_shutdown = match self.doc_binding {
None => Ok(()),
Some(binding) => binding.shutdown(),
};
if let Err(ref error) = doc_shutdown {
tracing::error!(%error, "document-service shutdown failed");
}
let host_shutdown = self.runtime.shutdown(self.logger);
if let Err(ref error) = host_shutdown {
tracing::error!(%error, "frame-core ordered shutdown failed");
}
let announcer_shutdown = match self.announcer {
None => Ok(()),
Some(announcer) => {
if host_shutdown.is_ok() {
match announcer.stop() {
Ok(None) => Ok(()),
Ok(Some(death)) => {
tracing::warn!(
death,
"the application-event announcer had died at runtime; its \
recorded death detail is reported here at teardown"
);
Ok(())
}
Err(error) => Err(error),
}
} else {
tracing::error!(
"component shutdown failed; abandoning the announcer pump un-joined \
rather than hanging teardown on transitions that will never arrive"
);
drop(announcer);
Ok(())
}
}
};
if let Err(ref error) = announcer_shutdown {
tracing::error!(%error, "announcer shutdown failed");
}
let liminal_shutdown = self.liminal.shutdown();
if let Err(ref error) = liminal_shutdown {
tracing::error!(%error, "embedded bus graceful shutdown failed");
}
doc_shutdown?;
host_shutdown?;
announcer_shutdown?;
liminal_shutdown?;
Ok(())
}
}
fn connect_announcer(
config: &FrameConfig,
liminal: &EmbeddedLiminal,
subscription: Option<LifecycleSubscription>,
) -> Result<Option<Announcer>, HostError> {
let (Some(channel), Some(subscription)) = (&config.frame.channel, subscription) else {
tracing::warn!(
"no [frame].channel is declared: the application-event announcer is disabled and \
the served page will observe no host events on any channel; declare \
[frame].channel to announce lifecycle transitions, capability denials, and \
application facts"
);
return Ok(None);
};
let auth_token = config
.bus
.auth
.as_ref()
.map(|auth| auth.token.clone().into_bytes());
Announcer::connect(
&liminal.tcp_addr().to_string(),
auth_token.as_deref(),
channel.clone(),
subscription,
)
.map(Some)
}
fn announce_boot_facts(
announce: crate::spec::FactAnnouncement,
runtime: &HostRuntime,
announcer: Option<&Announcer>,
) -> Result<(), HostError> {
let Some(announcer) = announcer else {
tracing::warn!("application facts are not announced: no [frame].channel is declared");
return Ok(());
};
announce(runtime.registry(), announcer).map_err(|source| HostError::Application {
stage: "announce",
source,
})
}
fn teardown_after_boot_failure(
doc_binding: Option<DocBinding>,
liminal: EmbeddedLiminal,
runtime: HostRuntime,
logger: EventLoggerHandle,
failed_stage: &str,
) {
if let Some(binding) = doc_binding
&& let Err(teardown) = binding.shutdown()
{
tracing::error!(
%teardown,
failed_stage,
"document-service teardown after boot failure also failed"
);
}
if let Err(teardown) = liminal.shutdown() {
tracing::error!(
%teardown,
failed_stage,
"embedded bus teardown after boot failure also failed"
);
}
if let Err(teardown) = runtime.shutdown(logger) {
tracing::error!(
%teardown,
failed_stage,
"frame-core teardown after boot failure also failed"
);
}
}
pub fn run_application(config: &FrameConfig, spec: AppSpec) -> Result<(), HostError> {
let app = Application::boot(config, spec)?;
let serve_outcome = app.serve(config, std::future::pending());
let shutdown_outcome = app.shutdown();
let stop = serve_outcome?;
if let ServeStop::LiminalExited { detail } = stop {
return Err(HostError::LiminalExited { detail });
}
shutdown_outcome?;
tracing::info!("frame server exited cleanly");
Ok(())
}