use std::collections::HashMap;
use std::collections::VecDeque;
use std::collections::hash_map::Entry;
use std::sync::{Arc, Mutex, Weak};
use beamr::atom::Atom;
use beamr::native::native_process::{NativeContext, NativeHandler, NativeOutcome};
use beamr::scheduler::Scheduler;
use super::actor::ActorCore;
use super::handoff::ConsumptionReceipt;
use crate::conversation::types::ParticipantPid;
use crate::envelope::Envelope;
use crate::error::LiminalError;
pub trait ParticipantBehaviour: Send + Sync + std::fmt::Debug {
fn process(&self, request: &Envelope) -> Option<Envelope>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct EchoBehaviour;
impl ParticipantBehaviour for EchoBehaviour {
fn process(&self, request: &Envelope) -> Option<Envelope> {
Some(request.clone())
}
}
#[derive(Debug, Default)]
pub(super) struct ParticipantRuntime {
registrations: Mutex<HashMap<u64, ParticipantRegistration>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ReplyExpectation {
Local,
Operation(u64),
None,
}
#[derive(Debug)]
pub(super) struct ForwardedRequest {
envelope: Envelope,
reply_expectation: ReplyExpectation,
handoff: Option<Arc<ConsumptionReceipt>>,
}
pub(super) type Mailbox = Mutex<VecDeque<ForwardedRequest>>;
type RequestQueue = Arc<Mailbox>;
type ParticipantSnapshot = (RequestQueue, Arc<dyn ParticipantBehaviour>, Weak<ActorCore>);
struct ParticipantRegistration {
inbox: RequestQueue,
behaviour: Arc<dyn ParticipantBehaviour>,
core: Weak<ActorCore>,
#[cfg(test)]
consume_gate: Option<Arc<ConsumeGate>>,
}
impl std::fmt::Debug for ParticipantRegistration {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ParticipantRegistration")
.field("behaviour", &self.behaviour)
.finish_non_exhaustive()
}
}
impl ParticipantRuntime {
pub(super) fn register(
&self,
pid: ParticipantPid,
inbox: RequestQueue,
behaviour: Arc<dyn ParticipantBehaviour>,
core: Weak<ActorCore>,
) -> Result<(), LiminalError> {
let registration = ParticipantRegistration {
inbox,
behaviour,
core,
#[cfg(test)]
consume_gate: None,
};
match lock(&self.registrations)?.entry(pid.get()) {
Entry::Occupied(mut entry) => {
if entry.get().core.strong_count() > 0 {
return Err(LiminalError::ConversationFailed {
message: format!(
"participant pid {} is already registered to a live conversation",
pid.get()
),
});
}
entry.insert(registration);
Ok(())
}
Entry::Vacant(entry) => {
entry.insert(registration);
Ok(())
}
}
}
pub(super) fn deregister(&self, pid: ParticipantPid) {
if let Ok(mut registrations) = self.registrations.lock() {
registrations.remove(&pid.get());
}
}
pub(super) fn reap_orphans(&self, scheduler: &Scheduler) {
if let Ok(mut registrations) = self.registrations.lock() {
registrations.retain(|&pid, registration| {
if registration.core.strong_count() > 0 {
return true;
}
scheduler.terminate_process(pid, beamr::process::ExitReason::Normal);
false
});
}
}
pub(super) fn registration_count(&self) -> usize {
self.registrations
.lock()
.map_or(0, |registrations| registrations.len())
}
fn run_slice(&self, pid: u64) -> Result<usize, LiminalError> {
let Some((inbox, behaviour, core)) = self.snapshot(pid) else {
return Ok(0);
};
#[cfg(test)]
let gate = self.consume_gate(pid)?;
#[cfg(test)]
if let Some(gate) = &gate {
if !gate.permitted.load(std::sync::atomic::Ordering::Acquire) {
if !gate.parked.swap(true, std::sync::atomic::Ordering::AcqRel) {
if let Err(error) = gate.parked_signal.send(()) {
tracing::error!(%error, "consumer parked signal disconnected");
}
}
return Ok(0);
}
}
let mut processed = 0;
loop {
#[cfg(test)]
if let Some(gate) = &gate {
if gate.taken.load(std::sync::atomic::Ordering::Acquire)
>= gate.limit.load(std::sync::atomic::Ordering::Acquire)
{
return Ok(processed);
}
}
let (request, notification) = {
let mut queue = lock(&inbox)?;
let receipt = queue.front().and_then(|request| request.handoff.clone());
let result = if let Some(receipt) = receipt {
if let Some((request, notification)) =
receipt.consume(ParticipantPid::new(pid), || {
let request = queue.pop_front();
#[cfg(test)]
if request.is_some() {
if let Some(gate) = &gate {
gate.taken
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
request
})?
{
(Some(request), Some(notification))
} else {
drop(queue.pop_front());
drop(queue);
continue;
}
} else {
let request = queue.pop_front();
#[cfg(test)]
if request.is_some() {
if let Some(gate) = &gate {
gate.taken
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
(request, None)
};
drop(queue);
result
};
let Some(request) = request else { break };
processed += 1;
if let Some(notification) = notification {
notification.publish();
}
if let Some(reply) = behaviour.process(&request.envelope) {
if request.reply_expectation == ReplyExpectation::None {
continue;
}
if let Some(core) = core.upgrade() {
let _ = core.deliver_participant_reply(request.reply_expectation, reply);
}
}
}
Ok(processed)
}
#[cfg(test)]
pub(super) fn hold_consumption(
&self,
pid: ParticipantPid,
scheduler: Arc<Scheduler>,
wakeup: Atom,
) -> Result<(ConsumptionPermit, std::sync::mpsc::Receiver<()>), LiminalError> {
let (parked_signal, parked) = std::sync::mpsc::channel();
let gate = Arc::new(ConsumeGate {
permitted: std::sync::atomic::AtomicBool::new(false),
parked: std::sync::atomic::AtomicBool::new(false),
taken: std::sync::atomic::AtomicU64::new(0),
limit: std::sync::atomic::AtomicU64::new(0),
parked_signal,
});
let mut registrations = lock(&self.registrations)?;
let registration =
registrations
.get_mut(&pid.get())
.ok_or_else(|| LiminalError::DeliveryFailed {
message: format!(
"participant {} has no registered forwarding queue",
pid.get()
),
})?;
registration.consume_gate = Some(Arc::clone(&gate));
drop(registrations);
Ok((
ConsumptionPermit {
pid,
scheduler,
wakeup,
gate,
},
parked,
))
}
#[cfg(test)]
fn consume_gate(&self, pid: u64) -> Result<Option<Arc<ConsumeGate>>, LiminalError> {
Ok(lock(&self.registrations)?
.get(&pid)
.and_then(|entry| entry.consume_gate.clone()))
}
pub(super) fn forwarding_channel(
&self,
participant: ParticipantPid,
) -> Result<ParticipantChannel, LiminalError> {
let registrations = lock(&self.registrations)?;
let registration =
registrations
.get(&participant.get())
.ok_or_else(|| LiminalError::DeliveryFailed {
message: format!(
"participant {} has no registered forwarding queue",
participant.get()
),
})?;
let owner = registration
.core
.upgrade()
.ok_or_else(|| LiminalError::DeliveryFailed {
message: format!(
"participant {} forwarding conversation is no longer live",
participant.get()
),
})?;
if owner.is_finalized() {
return Err(LiminalError::DeliveryFailed {
message: format!(
"participant {} forwarding conversation is closed",
participant.get()
),
});
}
let channel = ParticipantChannel::new(participant, Arc::clone(®istration.inbox));
drop(registrations);
Ok(channel)
}
fn snapshot(&self, pid: u64) -> Option<ParticipantSnapshot> {
let registrations = self.registrations.lock().ok()?;
let registration = registrations.get(&pid)?;
let snapshot = (
Arc::clone(®istration.inbox),
Arc::clone(®istration.behaviour),
registration.core.clone(),
);
drop(registrations);
Some(snapshot)
}
}
#[derive(Clone, Debug)]
pub(super) struct ParticipantChannel {
pid: ParticipantPid,
inbox: RequestQueue,
}
impl ParticipantChannel {
pub(super) const fn new(pid: ParticipantPid, inbox: RequestQueue) -> Self {
Self { pid, inbox }
}
pub(super) const fn pid(&self) -> ParticipantPid {
self.pid
}
pub(super) fn inbox_arc(&self) -> RequestQueue {
Arc::clone(&self.inbox)
}
pub(super) fn forward(
&self,
request: Envelope,
reply_expectation: ReplyExpectation,
scheduler: &Scheduler,
wakeup_atom: Atom,
) -> Result<(), LiminalError> {
lock(&self.inbox)?.push_back(ForwardedRequest {
envelope: request,
reply_expectation,
handoff: None,
});
if scheduler.enqueue_atom_message(self.pid.get(), wakeup_atom) {
Ok(())
} else {
lock(&self.inbox)?.pop_back();
Err(LiminalError::DeliveryFailed {
message: format!("conversation participant {} is not live", self.pid.get()),
})
}
}
pub(super) fn forward_dispatch(
&self,
request: Envelope,
receipt: &Arc<ConsumptionReceipt>,
scheduler: &Scheduler,
wakeup_atom: Atom,
) -> Result<(), LiminalError> {
if receipt.participant() != self.pid {
return Err(LiminalError::DeliveryFailed {
message: "dispatch forwarding target does not match its acceptance receipt"
.to_owned(),
});
}
let mut queue = lock(&self.inbox)?;
receipt.enqueue(&self.inbox, || {
queue.push_back(ForwardedRequest {
envelope: request,
reply_expectation: ReplyExpectation::Local,
handoff: Some(Arc::clone(receipt)),
});
if scheduler.enqueue_atom_message(self.pid.get(), wakeup_atom) {
Ok(())
} else {
queue.pop_back();
Err(LiminalError::DeliveryFailed {
message: format!("conversation participant {} is not live", self.pid.get()),
})
}
})
}
}
pub(super) struct ParticipantProcess {
runtime: Arc<ParticipantRuntime>,
wakeup_atom: Atom,
}
impl ParticipantProcess {
pub(super) const fn new(runtime: Arc<ParticipantRuntime>, wakeup_atom: Atom) -> Self {
Self {
runtime,
wakeup_atom,
}
}
}
impl std::fmt::Debug for ParticipantProcess {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ParticipantProcess")
.field("wakeup_atom", &self.wakeup_atom)
.finish_non_exhaustive()
}
}
impl NativeHandler for ParticipantProcess {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
let pid = ctx.self_pid();
while ctx.recv().is_some() {}
match self.runtime.run_slice(pid) {
Ok(_) => NativeOutcome::Wait,
Err(error) => {
tracing::error!(pid, %error, "participant request consumption failed");
NativeOutcome::Stop(beamr::process::ExitReason::Error)
}
}
}
}
fn lock<T>(mutex: &Mutex<T>) -> Result<std::sync::MutexGuard<'_, T>, LiminalError> {
mutex.lock().map_err(|error| LiminalError::DeliveryFailed {
message: format!("participant queue lock poisoned: {error}"),
})
}
#[cfg(test)]
#[derive(Debug)]
struct ConsumeGate {
permitted: std::sync::atomic::AtomicBool,
parked: std::sync::atomic::AtomicBool,
taken: std::sync::atomic::AtomicU64,
limit: std::sync::atomic::AtomicU64,
parked_signal: std::sync::mpsc::Sender<()>,
}
#[cfg(test)]
pub(crate) struct ConsumptionPermit {
pid: ParticipantPid,
scheduler: Arc<Scheduler>,
wakeup: Atom,
gate: Arc<ConsumeGate>,
}
#[cfg(test)]
impl std::fmt::Debug for ConsumptionPermit {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ConsumptionPermit")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
#[cfg(test)]
impl ConsumptionPermit {
pub(crate) fn release(&self) -> Result<(), LiminalError> {
self.permit_until(u64::MAX)
}
pub(crate) fn release_one(&self) -> Result<(), LiminalError> {
let limit =
self.messages_taken()
.checked_add(1)
.ok_or_else(|| LiminalError::DeliveryFailed {
message: "controlled consumer consumption count exhausted".to_owned(),
})?;
self.permit_until(limit)
}
fn permit_until(&self, limit: u64) -> Result<(), LiminalError> {
self.gate
.limit
.store(limit, std::sync::atomic::Ordering::Release);
self.gate
.permitted
.store(true, std::sync::atomic::Ordering::Release);
if self
.scheduler
.enqueue_atom_message(self.pid.get(), self.wakeup)
{
Ok(())
} else {
Err(LiminalError::DeliveryFailed {
message: "controlled consumer exited before consumption was permitted".to_owned(),
})
}
}
pub(crate) fn messages_taken(&self) -> u64 {
self.gate.taken.load(std::sync::atomic::Ordering::Acquire)
}
}
pub(super) fn discard_dispatch(inbox: &Mailbox, attempt: u64) -> Result<(), LiminalError> {
lock(inbox)?.retain(|request| {
request
.handoff
.as_ref()
.is_none_or(|receipt| receipt.attempt() != attempt)
});
Ok(())
}