pub(crate) mod beam;
mod construct;
mod departures;
pub(crate) mod queue;
#[cfg(test)]
mod scheduler_weak_tests;
mod wait;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::sync::{Arc, Mutex, MutexGuard, Weak};
use beamr::atom::Atom;
use beamr::native::{ProcessContext, ProcessInfoItem};
use beamr::scheduler::Scheduler;
use serde_json::Value;
use crate::causal::CausalContext;
use crate::channel::admission::PressureAggregate;
use crate::channel::record::{Recipient, recorded_now};
use crate::channel::schema::{Schema, SchemaId, SchemaValidationError};
use crate::channel::subscription::SubscriberRegistration;
use crate::envelope::{Envelope, PublisherId};
use crate::error::LiminalError;
const MAX_PENDING_COMMANDS: usize = 4_096;
type Unqueued = (LiminalError, Option<Box<ChannelCommandKind>>);
pub(crate) struct PendingPublish {
response: mpsc::Receiver<Result<PublishOutcome, LiminalError>>,
pid: u64,
}
pub(crate) use beam::{ActorRuntime, actor_module, private_data};
pub(crate) use departures::Departures;
use queue::{ChannelCommand, ChannelCommandKind, SubscriberSummary};
pub(crate) use queue::{PublishOutcome, predicate_from};
pub(crate) struct ChannelActorCore {
scheduler: Weak<Scheduler>,
command_atom: Atom,
schema: Mutex<Schema>,
subscribers: Mutex<Vec<SubscriberRegistration>>,
closed: Mutex<bool>,
commands: Mutex<VecDeque<ChannelCommand>>,
current_pid: Mutex<Option<u64>>,
restart_lock: Mutex<()>,
next_command_id: AtomicU64,
departures: Departures,
}
impl std::fmt::Debug for ChannelActorCore {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ChannelActorCore")
.field("current_pid", &self.current_pid.lock().ok().map(|pid| *pid))
.finish_non_exhaustive()
}
}
impl ChannelActorCore {
pub(crate) fn scheduler(&self) -> Result<Arc<Scheduler>, LiminalError> {
self.scheduler
.upgrade()
.ok_or_else(|| LiminalError::DeliveryFailed {
message: "channel actor scheduler is no longer available".to_owned(),
})
}
pub(crate) const fn restart_lock(&self) -> &Mutex<()> {
&self.restart_lock
}
pub(crate) fn set_current_pid(&self, pid: u64) -> Result<(), LiminalError> {
*lock(&self.current_pid)? = Some(pid);
Ok(())
}
pub(crate) fn current_pid(&self) -> Result<Option<u64>, LiminalError> {
Ok(*lock(&self.current_pid)?)
}
pub(crate) fn boot(&self) -> Result<(), LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let pid = self.enqueue(ChannelCommandKind::Boot { reply })?;
self.wait_live(&response, pid)?
}
pub(crate) fn enqueue_publish(
&self,
payload: Vec<u8>,
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
durable_position: Option<u64>,
recipients: Option<&mut Vec<Recipient>>,
) -> Result<PendingPublish, LiminalError> {
self.enqueue_identified(
payload,
publisher_id,
causal_context,
durable_position,
recipients,
None,
)
}
pub(crate) fn enqueue_identified(
&self,
payload: Vec<u8>,
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
durable_position: Option<u64>,
mut recipients: Option<&mut Vec<Recipient>>,
delivery_id: Option<crate::protocol::DeliveredId>,
) -> Result<PendingPublish, LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let queued = self.enqueue_returning(ChannelCommandKind::Publish {
delivery_id,
payload,
publisher_id,
causal_context,
durable_position,
recipients: recipients.as_deref_mut().map(std::mem::take),
reply,
});
match queued {
Ok(pid) => Ok(PendingPublish { response, pid }),
Err((error, unqueued)) => {
if let (
Some(slot),
Some(ChannelCommandKind::Publish {
recipients: Some(buffer),
..
}),
) = (recipients, unqueued.map(|kind| *kind))
{
*slot = buffer;
}
Err(error)
}
}
}
pub(crate) fn await_publish(
&self,
pending: PendingPublish,
) -> Result<PublishOutcome, LiminalError> {
let PendingPublish { response, pid } = pending;
self.wait_live(&response, pid)?
}
pub(crate) fn live_buffer_occupancy(&self) -> Result<(usize, usize), LiminalError> {
let mut total_queued: usize = 0;
let mut total_bound: usize = 0;
let subscribers = lock(&self.subscribers)?;
for subscriber in subscribers.iter() {
let (queued, bound) = subscriber.occupancy();
total_queued = total_queued.saturating_add(queued);
total_bound = total_bound.saturating_add(bound);
}
drop(subscribers);
Ok((total_queued, total_bound))
}
pub(crate) fn subscribe(
&self,
registration: SubscriberRegistration,
) -> Result<(), LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let pid = self.enqueue(ChannelCommandKind::Subscribe {
registration,
reply,
})?;
self.wait_live(&response, pid)?
}
pub(crate) fn unsubscribe(&self, pid: u64) -> Result<(), LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let target = self.enqueue(ChannelCommandKind::Unsubscribe { pid, reply })?;
self.wait_live(&response, target)?
}
pub(crate) fn evolve(
&self,
name: String,
field_schema: Value,
default: Value,
) -> Result<SchemaId, SchemaValidationError> {
let (reply, response) = mpsc::sync_channel(1);
let pid = self
.enqueue(ChannelCommandKind::Evolve {
name,
field_schema,
default,
reply,
})
.map_err(|error| SchemaValidationError::InvalidSchema {
message: error.to_string(),
})?;
self.wait_schema_live(&response, pid)?
}
pub(crate) fn schema_id(&self) -> Result<SchemaId, LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let pid = self.enqueue(ChannelCommandKind::SchemaId { reply })?;
self.wait_live(&response, pid)?
}
pub(crate) fn pre_publish_gate(&self, payload: &[u8]) -> Result<(), LiminalError> {
if *lock(&self.closed)? {
return Err(LiminalError::ChannelClosed {
message: "channel is closed".to_owned(),
});
}
lock(&self.schema)?
.validate(payload)
.map_err(|error| schema_mismatch(&error))
}
pub(crate) fn list_subscribers(&self) -> Result<SubscriberSummary, LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let pid = self.enqueue(ChannelCommandKind::ListSubscribers { reply })?;
self.wait_live(&response, pid)?
}
pub(crate) fn close(&self) -> Result<(), LiminalError> {
let (reply, response) = mpsc::sync_channel(1);
let pid = self.enqueue(ChannelCommandKind::Close { reply })?;
self.wait_live(&response, pid)?
}
fn wait_live<T>(
&self,
response: &mpsc::Receiver<Result<T, LiminalError>>,
pid: u64,
) -> Result<Result<T, LiminalError>, LiminalError> {
self.scheduler()?;
wait::wait_live(response, pid)
}
fn wait_schema_live(
&self,
response: &mpsc::Receiver<Result<SchemaId, SchemaValidationError>>,
pid: u64,
) -> Result<Result<SchemaId, SchemaValidationError>, SchemaValidationError> {
self.scheduler()
.map_err(|error| SchemaValidationError::InvalidSchema {
message: error.to_string(),
})?;
wait::wait_schema_live(response, pid)
}
fn enqueue(&self, kind: ChannelCommandKind) -> Result<u64, LiminalError> {
self.enqueue_returning(kind).map_err(|(error, _)| error)
}
fn enqueue_returning(&self, kind: ChannelCommandKind) -> Result<u64, Unqueued> {
let scheduler = match self.scheduler() {
Ok(scheduler) => scheduler,
Err(error) => return Err((error, Some(Box::new(kind)))),
};
let current = match lock(&self.current_pid) {
Ok(current) => *current,
Err(error) => return Err((error, Some(Box::new(kind)))),
};
let Some(pid) = current else {
return Err((
LiminalError::DeliveryFailed {
message: "channel actor has no live pid".to_owned(),
},
Some(Box::new(kind)),
));
};
let id = self.next_command_id.fetch_add(1, Ordering::Relaxed);
{
let mut commands = match lock(&self.commands) {
Ok(commands) => commands,
Err(error) => return Err((error, Some(Box::new(kind)))),
};
if commands.len() >= MAX_PENDING_COMMANDS {
drop(commands);
return Err((
LiminalError::DeliveryFailed {
message: format!(
"channel actor command queue is full ({MAX_PENDING_COMMANDS} pending)"
),
},
Some(Box::new(kind)),
));
}
commands.push_back(ChannelCommand { id, kind });
}
if scheduler.enqueue_atom_message(pid, self.command_atom) {
Ok(pid)
} else {
let removed = self.remove_command(id).map_err(|error| (error, None))?;
Err((
LiminalError::DeliveryFailed {
message: format!("channel actor pid {pid} is not live"),
},
removed.map(|command| Box::new(command.kind)),
))
}
}
fn remove_command(&self, id: u64) -> Result<Option<ChannelCommand>, LiminalError> {
let mut commands = lock(&self.commands)?;
Ok(commands
.iter()
.position(|command| command.id == id)
.and_then(|index| commands.remove(index)))
}
pub(crate) fn process_next_command(&self, context: &ProcessContext<'_>) -> bool {
let Some(command) = self.pop_command() else {
return false;
};
match command.kind {
ChannelCommandKind::Boot { reply } => {
let _ = reply.send(self.apply_boot(context));
false
}
ChannelCommandKind::Publish {
delivery_id,
payload,
publisher_id,
causal_context,
durable_position,
recipients,
reply,
} => {
let _ = reply.send(self.apply_publish(
&payload,
publisher_id,
causal_context,
durable_position,
recipients,
delivery_id,
));
false
}
ChannelCommandKind::Subscribe {
registration,
reply,
} => {
let _ = reply.send(self.apply_subscribe(registration, context));
false
}
ChannelCommandKind::Unsubscribe { pid, reply } => {
let _ = reply.send(self.apply_unsubscribe(pid, context));
false
}
ChannelCommandKind::Evolve {
name,
field_schema,
default,
reply,
} => {
let _ = reply.send(self.apply_evolve(name, field_schema, default));
false
}
ChannelCommandKind::SchemaId { reply } => {
let _ = reply.send(self.apply_schema_id());
false
}
ChannelCommandKind::ListSubscribers { reply } => {
let _ = reply.send(self.apply_list_subscribers());
false
}
ChannelCommandKind::Close { reply } => {
let result = self.apply_close();
let stop = result.is_ok();
let _ = reply.send(result);
stop
}
}
}
fn pop_command(&self) -> Option<ChannelCommand> {
self.commands.lock().ok()?.pop_front()
}
fn apply_boot(&self, context: &ProcessContext<'_>) -> Result<(), LiminalError> {
let process_info =
context
.process_info_facility()
.ok_or_else(|| LiminalError::DeliveryFailed {
message: "channel actor process-info facility is unavailable".to_owned(),
})?;
let registrations = std::mem::take(&mut *lock(&self.subscribers)?);
let mut live = Vec::with_capacity(registrations.len());
for subscriber in registrations {
if process_info
.process_info(subscriber.pid(), ProcessInfoItem::Status)
.is_none()
{
continue;
}
beam::link_subscriber(context, subscriber.pid())?;
live.push(subscriber);
}
*lock(&self.subscribers)? = live;
Ok(())
}
fn apply_publish(
&self,
payload: &[u8],
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
durable_position: Option<u64>,
mut recipients: Option<Vec<Recipient>>,
delivery_id: Option<crate::protocol::DeliveredId>,
) -> Result<PublishOutcome, LiminalError> {
if *lock(&self.closed)? {
return Err(LiminalError::ChannelClosed {
message: "channel is closed".to_owned(),
});
}
let mut envelope = {
let schema = lock(&self.schema)?;
schema
.validate(payload)
.map_err(|error| schema_mismatch(&error))?;
let normalized = schema
.validate_and_apply_defaults(payload)
.map_err(|error| schema_mismatch(&error))?;
Envelope::new(normalized, causal_context, schema.id(), publisher_id)
};
envelope.delivery_id = delivery_id;
let subscribers = lock(&self.subscribers)?;
let mut delivered_count = 0;
let mut matched_count = 0;
let mut aggregate = PressureAggregate::new();
for subscriber in subscribers.iter() {
let Some(outcome) = subscriber.deliver(&envelope, durable_position) else {
continue;
};
matched_count += 1;
if outcome.is_queued() {
delivered_count += 1;
if let Some(recipients) = recipients.as_mut() {
recipients.push((subscriber.pid(), recorded_now()));
}
}
if let Some(signal) = outcome.signal() {
aggregate.record(*signal);
} else if let Some(shed) = outcome.shed_signal() {
aggregate.record_dropped(*shed);
}
}
drop(subscribers);
Ok(PublishOutcome {
envelope,
delivered_count,
matched_count,
signal: aggregate.resolve(),
every_match_dropped: aggregate.every_match_dropped(),
recipients,
})
}
fn apply_subscribe(
&self,
registration: SubscriberRegistration,
context: &ProcessContext<'_>,
) -> Result<(), LiminalError> {
if *lock(&self.closed)? {
return Err(LiminalError::ChannelClosed {
message: "channel is closed".to_owned(),
});
}
beam::link_subscriber(context, registration.pid())?;
lock(&self.subscribers)?.push(registration);
Ok(())
}
fn apply_unsubscribe(
&self,
pid: u64,
context: &ProcessContext<'_>,
) -> Result<(), LiminalError> {
beam::unlink_subscriber(context, pid)?;
lock(&self.subscribers)?.retain(|subscriber| subscriber.pid() != pid);
Ok(())
}
fn apply_evolve(
&self,
name: String,
field_schema: Value,
default: Value,
) -> Result<SchemaId, SchemaValidationError> {
let mut schema =
self.schema
.lock()
.map_err(|error| SchemaValidationError::InvalidSchema {
message: format!("channel schema lock poisoned: {error}"),
})?;
let evolved = schema.evolve_add_field(name, field_schema, default)?;
let schema_id = evolved.id();
*schema = evolved;
drop(schema);
Ok(schema_id)
}
fn apply_schema_id(&self) -> Result<SchemaId, LiminalError> {
Ok(lock(&self.schema)?.id())
}
fn apply_list_subscribers(&self) -> Result<SubscriberSummary, LiminalError> {
Ok(lock(&self.subscribers)?
.iter()
.map(SubscriberRegistration::pid)
.collect())
}
fn apply_close(&self) -> Result<(), LiminalError> {
*lock(&self.closed)? = true;
lock(&self.subscribers)?.clear();
Ok(())
}
pub(crate) fn handle_subscriber_exit(&self, pid: u64) -> Result<(), LiminalError> {
let departed = {
let mut subscribers = lock(&self.subscribers)?;
let listed = subscribers.len();
subscribers.retain(|subscriber| subscriber.pid() != pid);
subscribers.len() < listed
};
if departed {
self.departures.report(pid);
}
Ok(())
}
}
fn schema_mismatch(error: &SchemaValidationError) -> LiminalError {
LiminalError::SchemaMismatch {
message: error.to_string(),
}
}
fn lock<T>(mutex: &Mutex<T>) -> Result<MutexGuard<'_, T>, LiminalError> {
mutex.lock().map_err(|error| LiminalError::DeliveryFailed {
message: format!("channel actor lock poisoned: {error}"),
})
}