use std::num::NonZeroUsize;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::Duration;
use beamr::module::ModuleRegistry;
use beamr::scheduler::{Scheduler, SchedulerConfig, SchedulerServices};
use frame_core::event::{
EventReceiveError, LifecycleEventKind, LifecycleState, LifecycleSubscription,
};
use frame_core::registry::ComponentRegistry;
use frame_core::status::ComponentStatus;
use frame_core::supervision::LifecycleConfig;
use crate::error::HostError;
use crate::manifest::{
CONTRACT_ID, LIVENESS_MESSAGE, PRESENCE_BYTECODE, PRESENCE_CHILD, component_id, component_meta,
};
const SCHEDULER_THREADS: usize = 2;
const OPERATION_TIMEOUT: Duration = Duration::from_secs(3);
const LIFECYCLE_EVENT_BUFFER: usize = 256;
const LOGGER_WAIT_QUANTUM: Duration = Duration::from_secs(3600);
pub struct HostRuntime {
scheduler: Arc<Scheduler>,
registry: ComponentRegistry,
}
pub struct EventLoggerHandle {
join: JoinHandle<()>,
}
impl HostRuntime {
pub fn new() -> Result<Self, HostError> {
let scheduler = Arc::new(
frame_core::composition::compose_scheduler(
SchedulerConfig {
thread_count: Some(SCHEDULER_THREADS),
..SchedulerConfig::default()
},
SchedulerServices::minimal(),
Arc::new(ModuleRegistry::new()),
)
.map_err(|source| HostError::Composition { source })?,
);
let registry = ComponentRegistry::new(
Arc::clone(&scheduler),
LifecycleConfig {
operation_timeout: OPERATION_TIMEOUT,
},
);
Ok(Self {
scheduler,
registry,
})
}
#[must_use]
pub const fn registry(&self) -> &ComponentRegistry {
&self.registry
}
#[must_use]
pub const fn scheduler(&self) -> &Arc<Scheduler> {
&self.scheduler
}
pub fn spawn_event_logger(&self) -> Result<EventLoggerHandle, HostError> {
let capacity =
NonZeroUsize::new(LIFECYCLE_EVENT_BUFFER).ok_or(HostError::SynchronizationPoisoned)?;
let subscription = self.registry.subscribe(capacity)?;
let join = std::thread::Builder::new()
.name("frame-host-lifecycle-log".to_owned())
.spawn(move || log_lifecycle_stream(&subscription))
.map_err(|source| HostError::EventLoggerSpawn { source })?;
Ok(EventLoggerHandle { join })
}
pub fn install_demo_component(&self) -> Result<ComponentStatus, HostError> {
let id = component_id();
let meta = component_meta();
tracing::info!(
component = %id,
name = %meta.name,
contract = %CONTRACT_ID,
"registering demo component"
);
self.registry.register(meta, PRESENCE_BYTECODE.to_vec())?;
let grants = self.registry.host_capabilities().grants_for(id)?;
tracing::info!(
component = %id,
grants = grants.len(),
"capability table registered component; class-(b) manifest declares no host-mediated needs"
);
self.registry.start(id)?;
let status = self
.registry
.status(id)?
.ok_or(HostError::StatusMissing { id })?;
if status.state != LifecycleState::Running {
return Err(HostError::NotRunning {
id,
state: status.state,
});
}
let answer = self.registry.probe_child(id, PRESENCE_CHILD)?;
if answer != LIVENESS_MESSAGE {
return Err(HostError::ProbeMismatch {
expected: LIVENESS_MESSAGE,
actual: answer,
});
}
for child in &status.children {
tracing::info!(
component = %id,
child = %child.name,
pid = child.pid,
"supervised child is live"
);
}
Ok(status)
}
pub fn shutdown(self, logger: EventLoggerHandle) -> Result<(), HostError> {
let id = component_id();
let stop_outcome = self.registry.stop(id)?;
tracing::info!(component = %id, outcome = ?stop_outcome, "ordered stop completed");
let remove_outcome = self.registry.remove(id)?;
tracing::info!(component = %id, outcome = ?remove_outcome, "component removed");
let residue = self.scheduler.process_count();
if residue != 0 {
return Err(HostError::ProcessResidue { count: residue });
}
self.scheduler.shutdown();
logger
.join
.join()
.map_err(|_| HostError::EventLoggerPanicked)
}
}
fn log_lifecycle_stream(subscription: &LifecycleSubscription) {
let mut reported_lag = 0;
loop {
let event = match subscription.recv_timeout(LOGGER_WAIT_QUANTUM) {
Ok(event) => event,
Err(EventReceiveError::Timeout) => continue,
Err(EventReceiveError::Closed) => {
tracing::info!("lifecycle event stream closed; console logger exiting");
return;
}
Err(EventReceiveError::Poisoned) => {
tracing::error!(
"lifecycle event stream synchronization poisoned; console logger exiting"
);
return;
}
};
let lagged = subscription.lagged_events();
if lagged > reported_lag {
tracing::warn!(
dropped = lagged - reported_lag,
total_dropped = lagged,
"lifecycle logger overflowed its bounded buffer; oldest events were dropped"
);
reported_lag = lagged;
}
match &event.kind {
LifecycleEventKind::Transition { from, to } => {
tracing::info!(
sequence = event.sequence,
component = %event.component_id,
from = ?from,
to = ?to,
"component lifecycle transition"
);
if event.component_id == component_id() && *to == LifecycleState::Removed {
tracing::info!("demo component removed; console logger exiting");
return;
}
}
LifecycleEventKind::CapabilityDenied(denial) => {
tracing::warn!(
sequence = event.sequence,
component = %denial.component_id,
kind = ?denial.kind,
scope = ?denial.scope,
declared = denial.declared,
"capability check denied"
);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::manifest;
#[test]
fn manifest_declares_the_contract_and_presence_child() {
let meta = manifest::component_meta();
assert_eq!(meta.provides.len(), 1);
assert_eq!(meta.provides[0].id, manifest::CONTRACT_ID);
assert_eq!(meta.children.len(), 1);
assert_eq!(meta.children[0].name, manifest::PRESENCE_CHILD);
assert!(
meta.needs.is_empty(),
"class-(b) manifest must declare no host needs"
);
assert!(meta.supervision.is_some());
}
}