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};
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::new(
324 SchedulerConfig {
325 thread_count: Some(1),
326 nif_private_data: Some(private_data),
327 ..SchedulerConfig::default()
328 },
329 registry,
330 NativeBifs::none(),
337 )
338 .map_err(|message| LiminalError::ConversationFailed { message })?;
339 Ok(Self {
340 shutting_down: std::sync::atomic::AtomicBool::new(false),
341 scheduler: Arc::new(scheduler),
342 runtime,
343 participant_runtime,
344 participant_wakeup_atom,
345 module_name,
346 entry_function,
347 #[cfg(test)]
348 boot_barrier: Mutex::new(None),
349 })
350 }
351
352 #[cfg(test)]
355 fn install_boot_barrier(&self, barrier: BootBarrier) {
356 if let Ok(mut slot) = self.boot_barrier.lock() {
357 *slot = Some(barrier);
358 }
359 }
360
361 fn spawn_participant(&self) -> Result<ParticipantChannel, LiminalError> {
367 let runtime = Arc::clone(&self.participant_runtime);
368 let wakeup_atom = self.participant_wakeup_atom;
369 let factory = Box::new(move || {
370 Box::new(ParticipantProcess::new(Arc::clone(&runtime), wakeup_atom))
371 as Box<dyn beamr::native::native_process::NativeHandler>
372 });
373 let pid = self.scheduler.spawn_native(factory).map_err(|error| {
374 LiminalError::ConversationFailed {
375 message: format!("failed to spawn conversation participant: {error}"),
376 }
377 })?;
378 let participant = ParticipantPid::new(pid);
379 let inbox = Arc::new(Mutex::new(VecDeque::new()));
380 Ok(ParticipantChannel::new(participant, inbox))
381 }
382
383 fn spawn_actor_for(
392 self: &Arc<Self>,
393 core: &Arc<ActorCore>,
394 ) -> Result<ParticipantPid, LiminalError> {
395 if core.is_finalized() {
396 return Err(LiminalError::ConversationFailed {
397 message: "conversation is closed".to_owned(),
398 });
399 }
400 let pid = self
401 .scheduler
402 .spawn_trap_exit(self.module_name, self.entry_function, Vec::new())
403 .map_err(|error| LiminalError::ConversationFailed {
404 message: format!("failed to spawn conversation actor: {error}"),
405 })?;
406 let actor = ParticipantPid::new(pid);
407 if let Err(error) = self.runtime.register(actor, Arc::downgrade(core)) {
408 self.rollback_actor_attempt(core, actor, None);
409 return Err(error);
410 }
411 let watcher = match self.spawn_watcher(core, actor) {
412 Ok(watcher) => watcher,
413 Err(error) => {
415 self.rollback_actor_attempt(core, actor, None);
416 return Err(error);
417 }
418 };
419 if let Err(error) = core
420 .set_watcher_pid(watcher)
421 .and_then(|()| core.set_current_pid(actor))
422 {
423 self.rollback_actor_attempt(core, actor, Some(watcher));
424 return Err(error);
425 }
426 #[cfg(test)]
427 self.boot_barrier_rendezvous(actor);
428 if let Err(error) = core.boot(actor) {
429 self.rollback_actor_attempt(core, actor, Some(watcher));
430 return Err(error);
431 }
432 if core.is_finalized() {
436 self.rollback_actor_attempt(core, actor, Some(watcher));
437 return Err(LiminalError::ConversationFailed {
438 message: "conversation is closed".to_owned(),
439 });
440 }
441 Ok(actor)
442 }
443
444 fn rollback_actor_attempt(
449 &self,
450 core: &ActorCore,
451 actor: ParticipantPid,
452 watcher: Option<ParticipantPid>,
453 ) {
454 if let Some(watcher) = watcher {
455 self.scheduler
456 .terminate_process(watcher.get(), beamr::process::ExitReason::Normal);
457 }
458 self.scheduler
459 .terminate_process(actor.get(), beamr::process::ExitReason::Normal);
460 self.runtime.deregister_owned(actor, core);
461 }
462
463 #[cfg(test)]
469 fn boot_barrier_rendezvous(&self, actor: ParticipantPid) {
470 let barrier = self
471 .boot_barrier
472 .lock()
473 .ok()
474 .and_then(|mut barrier| barrier.take());
475 if let Some((notify, proceed)) = barrier {
476 let _ = notify.send(actor);
477 let _ = proceed.recv();
478 }
479 }
480
481 fn spawn_watcher(
488 self: &Arc<Self>,
489 core: &Arc<ActorCore>,
490 actor: ParticipantPid,
491 ) -> Result<ParticipantPid, LiminalError> {
492 let (armed_tx, armed_rx) = mpsc::sync_channel::<()>(1);
493 let watcher_core = Arc::downgrade(core);
494 let watcher_supervisor = Arc::downgrade(self);
495 let factory = Box::new(move || {
496 Box::new(watcher::ActorExitWatcher::new(
497 watcher_core.clone(),
498 watcher_supervisor.clone(),
499 actor,
500 armed_tx.clone(),
501 )) as Box<dyn beamr::native::native_process::NativeHandler>
502 });
503 let watcher_pid = self.scheduler.spawn_native(factory).map_err(|error| {
504 LiminalError::ConversationFailed {
505 message: format!("failed to spawn conversation exit watcher: {error}"),
506 }
507 })?;
508 if armed_rx
511 .recv_timeout(std::time::Duration::from_secs(5))
512 .is_err()
513 {
514 self.scheduler
515 .terminate_process(watcher_pid, beamr::process::ExitReason::Normal);
516 return Err(LiminalError::ConversationFailed {
517 message: "conversation exit watcher failed to arm".to_owned(),
518 });
519 }
520 Ok(ParticipantPid::new(watcher_pid))
521 }
522}