use std::sync::atomic::AtomicU32;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_json::Value;
use crate::causal::CausalContext;
use crate::channel::actor::{ChannelActorCore, PendingPublish, predicate_from};
use crate::channel::admission::{
ChannelPressureConfig, defer_after_append, defer_delay, watermark_reached,
};
use crate::channel::durable_publish::{Outgoing, RuntimeDurable};
use crate::channel::observer::ClusterObserver;
use crate::channel::record::PublishRecord;
use crate::channel::schema::{Schema, SchemaId, SchemaValidationError};
use crate::channel::subscription::{InboxInstall, SubscriptionHandle, SubscriptionPredicate};
use crate::channel::supervisor::{ChannelSupervisor, shared_supervisor};
use crate::durability::bridge::block_on;
use crate::durability::{DurableChannel, DurableStore, recover_durable_channel};
use crate::envelope::{Envelope, PublisherId};
use crate::error::LiminalError;
use crate::pressure::PressureSignal;
const RUNTIME_DURABLE_PARTITIONS: usize = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChannelDelivery {
delivery_id: Option<crate::protocol::DeliveredId>,
delivered_count: usize,
matched_count: usize,
durable_position: Option<u64>,
signal: PressureSignal,
pressure_config: ChannelPressureConfig,
}
impl ChannelDelivery {
#[must_use]
pub const fn delivery_id(&self) -> Option<crate::protocol::DeliveredId> {
self.delivery_id
}
#[must_use]
pub const fn delivered_count(&self) -> usize {
self.delivered_count
}
#[must_use]
pub const fn matched_count(&self) -> usize {
self.matched_count
}
#[must_use]
pub const fn is_delivered(&self) -> bool {
self.delivered_count > 0
}
#[must_use]
pub const fn durable_position(&self) -> Option<u64> {
self.durable_position
}
#[must_use]
pub const fn pressure(&self) -> &PressureSignal {
&self.signal
}
#[must_use]
pub const fn is_admitted(&self) -> bool {
!matches!(self.signal, PressureSignal::Reject { .. })
}
#[must_use]
pub fn defer_delay(&self) -> Option<Duration> {
defer_delay(&self.signal, self.pressure_config)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChannelMode {
Ephemeral,
Durable,
}
pub type SchemaRef = Schema;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ChannelConfig {
pub name: String,
pub schema: Schema,
pub mode: ChannelMode,
pub pressure: ChannelPressureConfig,
}
impl ChannelConfig {
#[must_use]
pub const fn new(name: String, schema: Schema, mode: ChannelMode) -> Self {
Self {
name,
schema,
mode,
pressure: ChannelPressureConfig::DEFAULT,
}
}
#[must_use]
pub const fn with_pressure(mut self, pressure: ChannelPressureConfig) -> Self {
self.pressure = pressure;
self
}
}
struct ChannelActorState {
name: String,
supervisor: Result<ChannelSupervisor, String>,
core: OnceLock<Result<Arc<ChannelActorCore>, String>>,
restarts: AtomicU32,
}
impl ChannelActorState {
const fn new(name: String, supervisor: Result<ChannelSupervisor, String>) -> Self {
Self {
name,
supervisor,
core: OnceLock::new(),
restarts: AtomicU32::new(0),
}
}
fn supervisor(&self) -> Result<&ChannelSupervisor, LiminalError> {
self.supervisor
.as_ref()
.map_err(|message| LiminalError::PublishFailed {
message: format!("channel supervisor unavailable: {message}"),
})
}
fn observer(&self) -> Option<Arc<dyn ClusterObserver>> {
self.supervisor
.as_ref()
.ok()
.and_then(|supervisor| supervisor.observer().cloned())
}
fn core(&self, schema: &Schema) -> Result<Arc<ChannelActorCore>, LiminalError> {
let supervisor = self.supervisor()?;
let stored = self.core.get_or_init(|| {
supervisor
.spawn_channel(&self.name, schema.clone())
.map_err(|error| error.to_string())
});
let core = stored
.as_ref()
.map_err(|message| LiminalError::PublishFailed {
message: format!("channel actor unavailable: {message}"),
})?;
supervisor.ensure_running(core, &self.restarts)?;
Ok(Arc::clone(core))
}
}
impl std::fmt::Debug for ChannelActorState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ChannelActorState")
.field("supervisor", &self.supervisor)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct ChannelHandle {
delivery_identity: Arc<OnceLock<Arc<dyn super::PublicationIdentity>>>,
pub(super) config: ChannelConfig,
actor: Arc<ChannelActorState>,
pub(super) durable: Option<Arc<Mutex<RuntimeDurable>>>,
durable_store: Option<Arc<dyn DurableStore>>,
}
impl ChannelHandle {
pub fn install_publication_identity(
&self,
source: Arc<dyn super::PublicationIdentity>,
) -> Result<(), LiminalError> {
self.delivery_identity
.set(source)
.map_err(|_| LiminalError::PublishFailed {
message: "publication identity already installed".into(),
})
}
#[must_use]
pub fn new(config: ChannelConfig) -> Self {
let supervisor = shared_supervisor().map_err(|error| error.to_string());
let name = config.name.clone();
Self {
delivery_identity: Arc::new(OnceLock::new()),
config,
actor: Arc::new(ChannelActorState::new(name, supervisor)),
durable: None,
durable_store: None,
}
}
#[must_use]
pub fn with_supervisor(config: ChannelConfig, supervisor: ChannelSupervisor) -> Self {
let name = config.name.clone();
Self {
delivery_identity: Arc::new(OnceLock::new()),
config,
actor: Arc::new(ChannelActorState::new(name, Ok(supervisor))),
durable: None,
durable_store: None,
}
}
pub fn new_durable(
config: ChannelConfig,
store: Arc<dyn DurableStore>,
) -> Result<Self, LiminalError> {
let durable = recover_durable(&config.name, Arc::clone(&store))?;
let supervisor = shared_supervisor()?;
let name = config.name.clone();
Ok(Self {
delivery_identity: Arc::new(OnceLock::new()),
config,
actor: Arc::new(ChannelActorState::new(name, Ok(supervisor))),
durable: Some(Arc::new(Mutex::new(RuntimeDurable::new(durable)))),
durable_store: Some(store),
})
}
pub fn new_durable_with_supervisor(
config: ChannelConfig,
store: Arc<dyn DurableStore>,
supervisor: ChannelSupervisor,
) -> Result<Self, LiminalError> {
let durable = recover_durable(&config.name, Arc::clone(&store))?;
let name = config.name.clone();
Ok(Self {
delivery_identity: Arc::new(OnceLock::new()),
config,
actor: Arc::new(ChannelActorState::new(name, Ok(supervisor))),
durable: Some(Arc::new(Mutex::new(RuntimeDurable::new(durable)))),
durable_store: Some(store),
})
}
#[must_use]
pub const fn config(&self) -> &ChannelConfig {
&self.config
}
pub fn publish<Payload>(&self, payload: Payload) -> Result<(), LiminalError>
where
Payload: AsRef<[u8]>,
{
self.publish_with_context(payload, PublisherId::default(), None)
}
pub fn publish_from<Payload>(
&self,
publisher_id: impl Into<PublisherId>,
payload: Payload,
) -> Result<(), LiminalError>
where
Payload: AsRef<[u8]>,
{
self.publish_with_context(payload, publisher_id.into(), None)
}
pub fn publish_with_context<Payload>(
&self,
payload: Payload,
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
) -> Result<(), LiminalError>
where
Payload: AsRef<[u8]>,
{
self.publish_with_delivery(payload, publisher_id, causal_context)
.map(|_delivery| ())
}
pub fn publish_with_delivery<Payload>(
&self,
payload: Payload,
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
) -> Result<ChannelDelivery, LiminalError>
where
Payload: AsRef<[u8]>,
{
self.publish_with_key(payload, publisher_id, causal_context, None, None)
}
pub fn publish_recording<Payload>(
&self,
payload: Payload,
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
idempotency_key: Option<&str>,
record: &mut PublishRecord,
) -> Result<ChannelDelivery, LiminalError>
where
Payload: AsRef<[u8]>,
{
record.clear();
self.publish_with_key(
payload,
publisher_id,
causal_context,
idempotency_key,
Some(record),
)
}
pub(super) fn publish_with_key<Payload>(
&self,
payload: Payload,
publisher_id: PublisherId,
causal_context: Option<CausalContext>,
idempotency_key: Option<&str>,
mut record: Option<&mut PublishRecord>,
) -> Result<ChannelDelivery, LiminalError>
where
Payload: AsRef<[u8]>,
{
let core = self.core()?;
let delivery_id = self
.delivery_identity
.get()
.map(|source| source.next())
.transpose()?;
core.pre_publish_gate(payload.as_ref())?;
if let Some(mut rejection) = self.watermark_rejection(&core)? {
rejection.delivery_id = delivery_id;
return Ok(rejection);
}
let (durable_position, pending, committed) = match self.durable.as_ref() {
Some(durable) => {
let (position, pending, committed) = self.persist_and_enqueue(
&core,
durable,
Outgoing {
delivery_id,
payload: payload.as_ref(),
publisher_id,
causal_context,
idempotency_key,
record: record.as_deref_mut(),
},
)?;
(Some(position), pending, Some(committed))
}
None => (
None,
core.enqueue_identified(
payload.as_ref().to_vec(),
publisher_id,
causal_context,
None,
record.as_deref_mut().map(|record| &mut record.recipients),
delivery_id,
)?,
None,
),
};
let mut outcome = core
.await_publish(pending)
.map_err(|error| match committed {
Some(row) => row.unresolved(error),
None => error,
})?;
if let Some(observer) = self.actor.observer() {
observer.on_publish(&self.config.name, &outcome.envelope);
}
let signal = if durable_position.is_some() && !outcome.every_match_dropped {
defer_after_append(outcome.signal)
} else {
outcome.signal
};
if let (Some(record), Some(filled)) = (record, outcome.recipients.take()) {
record.recipients = filled;
}
Ok(ChannelDelivery {
delivery_id,
delivered_count: outcome.delivered_count,
matched_count: outcome.matched_count,
durable_position,
signal,
pressure_config: self.config.pressure,
})
}
pub(super) fn finish_recovered(
&self,
core: &ChannelActorCore,
pending: PendingPublish,
) -> Result<(), LiminalError> {
let outcome = core.await_publish(pending)?;
if let Some(observer) = self.actor.observer() {
observer.on_publish(&self.config.name, &outcome.envelope);
}
Ok(())
}
pub fn publish_batch_with_delivery(
&self,
payloads: &[Vec<u8>],
publisher: &PublisherId,
causal: Option<&CausalContext>,
) -> Result<Vec<ChannelDelivery>, LiminalError> {
if payloads.is_empty() {
return Ok(Vec::new());
}
if publisher.as_str().trim().is_empty() {
return Err(LiminalError::PublishFailed {
message: "durable batch publisher is empty".to_owned(),
});
}
let durable = self
.durable
.as_ref()
.ok_or_else(|| LiminalError::PublishFailed {
message: "batch publication requires a durable channel".to_owned(),
})?;
let core = self.core()?;
for payload in payloads {
core.pre_publish_gate(payload)?;
}
if let Some(rejection) = self.watermark_rejection(&core)? {
return Ok(vec![rejection; payloads.len()]);
}
let queued = self.persist_batch_and_enqueue(&core, durable, payloads, publisher, causal)?;
let mut deliveries = Vec::with_capacity(queued.len());
for (pending, mut proof) in queued {
let position = proof.sequence();
let outcome = core.await_publish(pending).map_err(|error| {
proof.set_cause(format!("batch committed; live delivery failed: {error}"));
LiminalError::PublishUnresolved { append: proof }
})?;
if let Some(observer) = self.actor.observer() {
observer.on_publish(&self.config.name, &outcome.envelope);
}
let signal = if outcome.every_match_dropped {
outcome.signal
} else {
defer_after_append(outcome.signal)
};
deliveries.push(ChannelDelivery {
delivery_id: outcome.envelope.delivery_id,
delivered_count: outcome.delivered_count,
matched_count: outcome.matched_count,
durable_position: Some(position),
signal,
pressure_config: self.config.pressure,
});
}
Ok(deliveries)
}
fn watermark_rejection(
&self,
core: &Arc<ChannelActorCore>,
) -> Result<Option<ChannelDelivery>, LiminalError> {
if self.durable.is_none() {
return Ok(None);
}
let (total_queued, total_bound) = core.live_buffer_occupancy()?;
if !watermark_reached(
total_queued,
total_bound,
self.config.pressure.durable_reject_watermark_percent,
) {
return Ok(None);
}
Ok(Some(ChannelDelivery {
delivery_id: None,
delivered_count: 0,
matched_count: 0,
durable_position: None,
signal: PressureSignal::reject(total_queued, total_bound, total_queued, total_bound),
pressure_config: self.config.pressure,
}))
}
fn durable_join_point(
durable: &Arc<Mutex<RuntimeDurable>>,
) -> Result<(String, u64), LiminalError> {
let channel = durable
.lock()
.map_err(|error| LiminalError::PublishFailed {
message: format!("durable channel state unavailable: {error}"),
})?;
let stream_key = channel.channel.stream_key_for(0);
let head = channel.channel.next_expected_sequence(0).unwrap_or(0);
drop(channel);
Ok((stream_key, head))
}
fn durable_store(&self) -> Result<Arc<dyn DurableStore>, LiminalError> {
self.durable_store
.as_ref()
.map(Arc::clone)
.ok_or_else(|| LiminalError::PublishFailed {
message: format!(
"channel '{}' has no durable store to replay from",
self.config.name
),
})
}
pub fn current_schema_id(&self) -> Result<SchemaId, LiminalError> {
self.core()?.schema_id()
}
pub fn evolve_schema_add_field(
&self,
name: impl Into<String>,
field_schema: Value,
default: Value,
) -> Result<SchemaId, SchemaValidationError> {
let core = self
.core()
.map_err(|error| SchemaValidationError::InvalidSchema {
message: error.to_string(),
})?;
core.evolve(name.into(), field_schema, default)
}
pub fn subscribe(&self) -> Result<SubscriptionHandle, LiminalError> {
self.subscribe_inner(None, None, None)
}
pub fn subscribe_with_install(
&self,
install: InboxInstall,
) -> Result<SubscriptionHandle, LiminalError> {
self.subscribe_inner(None, Some(install), None)
}
pub fn subscribe_filtered<F>(&self, predicate: F) -> Result<SubscriptionHandle, LiminalError>
where
F: Fn(&Envelope) -> bool + Send + Sync + 'static,
{
self.subscribe_inner(Some(predicate_from(predicate)), None, None)
}
pub fn subscribe_consumer(
&self,
consumer: &str,
install: InboxInstall,
) -> Result<SubscriptionHandle, LiminalError> {
self.subscribe_inner(None, Some(install), Some(consumer))
}
fn subscribe_inner(
&self,
predicate: Option<SubscriptionPredicate>,
install: Option<InboxInstall>,
consumer: Option<&str>,
) -> Result<SubscriptionHandle, LiminalError> {
let core = self.core()?;
let (handle, registration) =
SubscriptionHandle::spawn(&core.scheduler()?, predicate.clone(), install)?;
if let Some(durable) = self.durable.as_ref() {
let (stream_key, head) = Self::durable_join_point(durable)?;
handle.seed_replay_cursor(head);
handle.attach_durable_refill(
self.durable_store()?,
stream_key,
self.config.schema.id(),
predicate,
);
if let Some(consumer) = consumer {
handle.resume_consumer(consumer)?;
}
}
let pid = registration.pid();
core.subscribe(registration)?;
if consumer.is_some() {
handle.refill_if_lagging()?;
}
if let Some(observer) = self.actor.observer() {
observer.on_subscribe(&self.config.name, pid);
}
Ok(handle)
}
pub fn unsubscribe(&self, subscription: &SubscriptionHandle) -> Result<(), LiminalError> {
let pid = subscription.pid();
self.core()?.unsubscribe(pid)?;
if let Some(observer) = self.actor.observer() {
observer.on_unsubscribe(&self.config.name, pid);
}
Ok(())
}
pub fn flush(&self) -> Result<(), LiminalError> {
drop(self.core()?);
let Some(durable) = self.durable.as_ref() else {
return Ok(());
};
let flush_result = {
let channel = durable
.lock()
.map_err(|error| LiminalError::PublishFailed {
message: format!("durable channel state unavailable: {error}"),
})?;
block_on(channel.channel.flush_store())
};
flush_result
.map_err(|error| LiminalError::PublishFailed {
message: format!(
"durable flush bridge for channel '{}' failed: {error}",
self.config.name
),
})?
.map_err(|error| LiminalError::PublishFailed {
message: format!(
"durable flush for channel '{}' failed: {error}",
self.config.name
),
})?;
Ok(())
}
pub fn subscriber_count(&self) -> Result<usize, LiminalError> {
Ok(self.core()?.list_subscribers()?.len())
}
pub fn close(&self) -> Result<(), LiminalError> {
self.core()?.close()
}
#[must_use]
pub fn is_actor_spawned(&self) -> bool {
self.actor.core.get().is_some_and(Result::is_ok)
}
pub(super) fn core(&self) -> Result<Arc<ChannelActorCore>, LiminalError> {
self.actor.core(&self.config.schema)
}
#[cfg(test)]
pub(crate) fn actor_pid(&self) -> Result<u64, LiminalError> {
let core = self.core()?;
core.current_pid()?
.ok_or_else(|| LiminalError::DeliveryFailed {
message: "channel actor has no live pid".to_owned(),
})
}
#[cfg(test)]
pub(crate) fn scheduler(&self) -> Result<Arc<beamr::scheduler::Scheduler>, LiminalError> {
self.core()?.scheduler()
}
}
fn recover_durable(
channel_name: &str,
store: Arc<dyn DurableStore>,
) -> Result<DurableChannel, LiminalError> {
block_on(recover_durable_channel(
channel_name.to_owned(),
RUNTIME_DURABLE_PARTITIONS,
store,
))
.map_err(|error| LiminalError::PublishFailed {
message: format!("durable recovery bridge for channel '{channel_name}' failed: {error}"),
})?
.map_err(|error| LiminalError::PublishFailed {
message: format!("failed to recover durable channel '{channel_name}': {error}"),
})
}
pub(super) fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
})
}