1use std::any::Any;
2use std::collections::VecDeque;
3use std::sync::{Arc, Mutex, mpsc};
4use std::time::Instant;
5
6use beamr::atom::{Atom, AtomTable};
7use beamr::module::ModuleRegistry;
8use beamr::scheduler::{NativeBifs, Scheduler, SchedulerConfig, SchedulerServices};
9
10mod backend;
11mod beam;
12mod core;
13mod exit;
14mod queue;
15mod sync;
16mod watcher;
17
18use crate::conversation::participant::{
19 ParticipantBehaviour, ParticipantChannel, ParticipantProcess, ParticipantRuntime,
20};
21use crate::conversation::types::{
22 ConversationConfig, ConversationHandle, ConversationState, CrashPolicy, ParticipantPid,
23};
24use crate::envelope::Envelope;
25use crate::error::LiminalError;
26use backend::ActorBackend;
27use beam::{ActorRuntime, actor_module};
28pub(crate) use core::ActorCore;
29
30#[cfg(test)]
31mod tests;
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum ConversationCommand {
35 Send(Envelope),
36 Receive,
37 Close,
38 QueryState,
39}
40
41#[derive(Clone, Debug)]
42pub struct ConversationSupervisor {
43 inner: Arc<SupervisorInner>,
44}
45
46impl ConversationSupervisor {
47 pub fn new() -> Result<Self, LiminalError> {
50 SupervisorInner::new().map(|inner| Self {
51 inner: Arc::new(inner),
52 })
53 }
54
55 pub fn spawn(&self, config: ConversationConfig) -> Result<ConversationActor, LiminalError> {
65 let core = Arc::new(ActorCore::new(Arc::clone(&self.inner), config, Vec::new()));
66 self.inner.spawn_actor_for(&core)?;
67 let handle = ConversationHandle::new(Arc::new(ActorBackend {
68 core: Arc::clone(&core),
69 }));
70 Ok(ConversationActor { core, handle })
71 }
72
73 pub fn spawn_with_participant(
86 &self,
87 behaviour: Arc<dyn ParticipantBehaviour>,
88 timeout: Option<std::time::Duration>,
89 mode: crate::channel::ChannelMode,
90 on_crash: CrashPolicy,
91 ) -> Result<(ConversationActor, ParticipantPid), LiminalError> {
92 let channel = self.inner.spawn_participant()?;
93 let participant = channel.pid();
94 let config = ConversationConfig::new(vec![participant], timeout, mode, on_crash);
95 let core = Arc::new(ActorCore::new(
96 Arc::clone(&self.inner),
97 config,
98 vec![channel.clone()],
99 ));
100 self.inner.participant_runtime.register(
103 participant,
104 channel.inbox_arc(),
105 behaviour,
106 Arc::downgrade(&core),
107 )?;
108 if let Err(error) = self.inner.spawn_actor_for(&core) {
112 self.inner
113 .scheduler
114 .terminate_process(participant.get(), beamr::process::ExitReason::Normal);
115 self.inner.participant_runtime.deregister(participant);
116 return Err(error);
117 }
118 let handle = ConversationHandle::new(Arc::new(ActorBackend {
119 core: Arc::clone(&core),
120 }));
121 Ok((ConversationActor { core, handle }, participant))
122 }
123
124 #[cfg(test)]
125 pub(crate) fn hold_participant_consumption(
126 &self,
127 participant: ParticipantPid,
128 ) -> Result<(super::participant::ConsumptionPermit, mpsc::Receiver<()>), LiminalError> {
129 self.inner.participant_runtime.hold_consumption(
130 participant,
131 Arc::clone(&self.inner.scheduler),
132 self.inner.participant_wakeup_atom,
133 )
134 }
135
136 #[must_use]
138 pub fn scheduler(&self) -> Arc<Scheduler> {
139 Arc::clone(&self.inner.scheduler)
140 }
141
142 #[must_use]
147 pub fn registered_actor_count(&self) -> usize {
148 self.inner.runtime.registration_count()
149 }
150
151 #[must_use]
155 pub fn registered_participant_count(&self) -> usize {
156 self.inner.participant_runtime.registration_count()
157 }
158
159 pub fn shutdown(&self) {
161 self.inner
162 .shutting_down
163 .store(true, std::sync::atomic::Ordering::Release);
164 if let Err(error) = self.inner.runtime.close_dispatch_waiters() {
165 tracing::error!(%error, "closing dispatch waiters during supervisor shutdown failed");
166 }
167 self.inner.scheduler.shutdown();
168 }
169}
170
171#[derive(Clone, Debug)]
172pub struct ConversationActor {
173 core: Arc<ActorCore>,
174 handle: ConversationHandle,
175}
176
177impl ConversationActor {
178 pub(crate) fn register_dispatch_receipt(
179 &self,
180 receipt: Arc<super::handoff::ConsumptionReceipt>,
181 ) -> Result<super::handoff::DispatchRegistration, LiminalError> {
182 self.core.register_dispatch_receipt(receipt)
183 }
184
185 pub(crate) fn send_dispatch(
186 &self,
187 message: Envelope,
188 receipt: Arc<super::handoff::ConsumptionReceipt>,
189 ) -> Result<(), LiminalError> {
190 self.core.submit_dispatch(message, receipt)
191 }
192
193 #[must_use]
195 pub fn handle(&self) -> ConversationHandle {
196 self.handle.clone()
197 }
198
199 pub fn pid(&self) -> Result<ParticipantPid, LiminalError> {
204 self.core.ensure_running()
205 }
206
207 pub fn state(&self) -> Result<ConversationState, LiminalError> {
212 self.handle.query_state()
213 }
214
215 pub fn receive_timeout(&self, timeout: std::time::Duration) -> Result<Envelope, LiminalError> {
224 self.core.submit_receive_timeout(timeout)
225 }
226
227 #[must_use]
231 pub fn try_take_reply(&self) -> Option<(u64, Envelope)> {
232 self.core.try_take_reply()
233 }
234
235 #[must_use]
237 pub fn has_pending_reply(&self) -> bool {
238 self.core.has_pending_reply()
239 }
240
241 pub fn register_reply_notifier(&self, notifier: std::sync::Arc<dyn Fn() + Send + Sync>) {
245 self.core.register_reply_notifier(notifier);
246 }
247
248 pub fn finalize(&self) {
259 self.core.finalize();
260 }
261
262 pub fn notify_on_participant_exit(
272 &self,
273 participant: ParticipantPid,
274 notifier: mpsc::SyncSender<Instant>,
275 ) -> Result<(), LiminalError> {
276 self.core.register_exit_notifier(participant, notifier)
277 }
278}
279
280#[cfg(test)]
285type BootBarrier = (mpsc::Sender<ParticipantPid>, mpsc::Receiver<()>);
286
287struct SupervisorInner {
288 shutting_down: std::sync::atomic::AtomicBool,
289 scheduler: Arc<Scheduler>,
290 runtime: Arc<ActorRuntime>,
291 participant_runtime: Arc<ParticipantRuntime>,
292 participant_wakeup_atom: Atom,
293 module_name: Atom,
294 entry_function: Atom,
295 #[cfg(test)]
296 boot_barrier: Mutex<Option<BootBarrier>>,
297}
298
299impl std::fmt::Debug for SupervisorInner {
300 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 formatter
302 .debug_struct("SupervisorInner")
303 .field("runtime", &self.runtime)
304 .field("module_name", &self.module_name)
305 .field("entry_function", &self.entry_function)
306 .finish_non_exhaustive()
307 }
308}
309
310impl SupervisorInner {
311 fn new() -> Result<Self, LiminalError> {
312 let atoms = AtomTable::with_common_atoms();
313 let module_name = atoms.intern("liminal_conversation_actor");
314 let entry_function = atoms.intern("main");
315 let command_function = atoms.intern("process_command");
316 let command_atom = atoms.intern("liminal_conversation_command");
317 let participant_wakeup_atom = atoms.intern("liminal_conversation_participant_wakeup");
318 let runtime = Arc::new(ActorRuntime::new(command_atom));
319 let participant_runtime = Arc::new(ParticipantRuntime::default());
320 let registry = Arc::new(ModuleRegistry::new());
321 registry.insert(actor_module(module_name, entry_function, command_function));
322 let private_data: Arc<dyn Any + Send + Sync> = runtime.clone();
323 let scheduler = Scheduler::with_services(
326 SchedulerConfig {
327 thread_count: Some(1),
328 nif_private_data: Some(private_data),
329 ..SchedulerConfig::default()
330 },
331 SchedulerServices::minimal(),
332 registry,
333 NativeBifs::none(),
340 )
341 .map_err(|message| LiminalError::ConversationFailed { message })?;
342 Ok(Self {
343 shutting_down: std::sync::atomic::AtomicBool::new(false),
344 scheduler: Arc::new(scheduler),
345 runtime,
346 participant_runtime,
347 participant_wakeup_atom,
348 module_name,
349 entry_function,
350 #[cfg(test)]
351 boot_barrier: Mutex::new(None),
352 })
353 }
354
355 #[cfg(test)]
358 fn install_boot_barrier(&self, barrier: BootBarrier) {
359 if let Ok(mut slot) = self.boot_barrier.lock() {
360 *slot = Some(barrier);
361 }
362 }
363
364 fn spawn_participant(&self) -> Result<ParticipantChannel, LiminalError> {
370 let runtime = Arc::clone(&self.participant_runtime);
371 let wakeup_atom = self.participant_wakeup_atom;
372 let factory = Box::new(move || {
373 Box::new(ParticipantProcess::new(Arc::clone(&runtime), wakeup_atom))
374 as Box<dyn beamr::native::native_process::NativeHandler>
375 });
376 let pid = self.scheduler.spawn_native(factory).map_err(|error| {
377 LiminalError::ConversationFailed {
378 message: format!("failed to spawn conversation participant: {error}"),
379 }
380 })?;
381 let participant = ParticipantPid::new(pid);
382 let inbox = Arc::new(Mutex::new(VecDeque::new()));
383 Ok(ParticipantChannel::new(participant, inbox))
384 }
385
386 fn spawn_actor_for(
395 self: &Arc<Self>,
396 core: &Arc<ActorCore>,
397 ) -> Result<ParticipantPid, LiminalError> {
398 if core.is_finalized() {
399 return Err(LiminalError::ConversationFailed {
400 message: "conversation is closed".to_owned(),
401 });
402 }
403 let pid = self
404 .scheduler
405 .spawn_trap_exit(self.module_name, self.entry_function, Vec::new())
406 .map_err(|error| LiminalError::ConversationFailed {
407 message: format!("failed to spawn conversation actor: {error}"),
408 })?;
409 let actor = ParticipantPid::new(pid);
410 if let Err(error) = self.runtime.register(actor, Arc::downgrade(core)) {
411 self.rollback_actor_attempt(core, actor, None);
412 return Err(error);
413 }
414 let watcher = match self.spawn_watcher(core, actor) {
415 Ok(watcher) => watcher,
416 Err(error) => {
418 self.rollback_actor_attempt(core, actor, None);
419 return Err(error);
420 }
421 };
422 if let Err(error) = core
423 .set_watcher_pid(watcher)
424 .and_then(|()| core.set_current_pid(actor))
425 {
426 self.rollback_actor_attempt(core, actor, Some(watcher));
427 return Err(error);
428 }
429 #[cfg(test)]
430 self.boot_barrier_rendezvous(actor);
431 if let Err(error) = core.boot(actor) {
432 self.rollback_actor_attempt(core, actor, Some(watcher));
433 return Err(error);
434 }
435 if core.is_finalized() {
439 self.rollback_actor_attempt(core, actor, Some(watcher));
440 return Err(LiminalError::ConversationFailed {
441 message: "conversation is closed".to_owned(),
442 });
443 }
444 Ok(actor)
445 }
446
447 fn rollback_actor_attempt(
452 &self,
453 core: &ActorCore,
454 actor: ParticipantPid,
455 watcher: Option<ParticipantPid>,
456 ) {
457 if let Some(watcher) = watcher {
458 self.scheduler
459 .terminate_process(watcher.get(), beamr::process::ExitReason::Normal);
460 }
461 self.scheduler
462 .terminate_process(actor.get(), beamr::process::ExitReason::Normal);
463 self.runtime.deregister_owned(actor, core);
464 }
465
466 #[cfg(test)]
472 fn boot_barrier_rendezvous(&self, actor: ParticipantPid) {
473 let barrier = self
474 .boot_barrier
475 .lock()
476 .ok()
477 .and_then(|mut barrier| barrier.take());
478 if let Some((notify, proceed)) = barrier {
479 let _ = notify.send(actor);
480 let _ = proceed.recv();
481 }
482 }
483
484 fn spawn_watcher(
491 self: &Arc<Self>,
492 core: &Arc<ActorCore>,
493 actor: ParticipantPid,
494 ) -> Result<ParticipantPid, LiminalError> {
495 let (armed_tx, armed_rx) = mpsc::sync_channel::<()>(1);
496 let watcher_core = Arc::downgrade(core);
497 let watcher_supervisor = Arc::downgrade(self);
498 let factory = Box::new(move || {
499 Box::new(watcher::ActorExitWatcher::new(
500 watcher_core.clone(),
501 watcher_supervisor.clone(),
502 actor,
503 armed_tx.clone(),
504 )) as Box<dyn beamr::native::native_process::NativeHandler>
505 });
506 let watcher_pid = self.scheduler.spawn_native(factory).map_err(|error| {
507 LiminalError::ConversationFailed {
508 message: format!("failed to spawn conversation exit watcher: {error}"),
509 }
510 })?;
511 if armed_rx
514 .recv_timeout(std::time::Duration::from_secs(5))
515 .is_err()
516 {
517 self.scheduler
518 .terminate_process(watcher_pid, beamr::process::ExitReason::Normal);
519 return Err(LiminalError::ConversationFailed {
520 message: "conversation exit watcher failed to arm".to_owned(),
521 });
522 }
523 Ok(ParticipantPid::new(watcher_pid))
524 }
525}