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 #[must_use]
126 pub fn scheduler(&self) -> Arc<Scheduler> {
127 Arc::clone(&self.inner.scheduler)
128 }
129
130 #[must_use]
135 pub fn registered_actor_count(&self) -> usize {
136 self.inner.runtime.registration_count()
137 }
138
139 #[must_use]
143 pub fn registered_participant_count(&self) -> usize {
144 self.inner.participant_runtime.registration_count()
145 }
146
147 pub fn shutdown(&self) {
149 self.inner.scheduler.shutdown();
150 }
151}
152
153#[derive(Clone, Debug)]
154pub struct ConversationActor {
155 core: Arc<ActorCore>,
156 handle: ConversationHandle,
157}
158
159impl ConversationActor {
160 #[must_use]
162 pub fn handle(&self) -> ConversationHandle {
163 self.handle.clone()
164 }
165
166 pub fn pid(&self) -> Result<ParticipantPid, LiminalError> {
171 self.core.ensure_running()
172 }
173
174 pub fn state(&self) -> Result<ConversationState, LiminalError> {
179 self.handle.query_state()
180 }
181
182 pub fn receive_timeout(&self, timeout: std::time::Duration) -> Result<Envelope, LiminalError> {
191 self.core.submit_receive_timeout(timeout)
192 }
193
194 #[must_use]
198 pub fn try_take_reply(&self) -> Option<Envelope> {
199 self.core.try_take_reply()
200 }
201
202 #[must_use]
204 pub fn has_pending_reply(&self) -> bool {
205 self.core.has_pending_reply()
206 }
207
208 pub fn register_reply_notifier(&self, notifier: std::sync::Arc<dyn Fn() + Send + Sync>) {
212 self.core.register_reply_notifier(notifier);
213 }
214
215 pub fn finalize(&self) {
226 self.core.finalize();
227 }
228
229 pub fn notify_on_participant_exit(
239 &self,
240 participant: ParticipantPid,
241 notifier: mpsc::SyncSender<Instant>,
242 ) -> Result<(), LiminalError> {
243 self.core.register_exit_notifier(participant, notifier)
244 }
245}
246
247#[cfg(test)]
252type BootBarrier = (mpsc::Sender<ParticipantPid>, mpsc::Receiver<()>);
253
254struct SupervisorInner {
255 scheduler: Arc<Scheduler>,
256 runtime: Arc<ActorRuntime>,
257 participant_runtime: Arc<ParticipantRuntime>,
258 participant_wakeup_atom: Atom,
259 module_name: Atom,
260 entry_function: Atom,
261 #[cfg(test)]
262 boot_barrier: Mutex<Option<BootBarrier>>,
263}
264
265impl std::fmt::Debug for SupervisorInner {
266 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 formatter
268 .debug_struct("SupervisorInner")
269 .field("runtime", &self.runtime)
270 .field("module_name", &self.module_name)
271 .field("entry_function", &self.entry_function)
272 .finish_non_exhaustive()
273 }
274}
275
276impl SupervisorInner {
277 fn new() -> Result<Self, LiminalError> {
278 let atoms = AtomTable::with_common_atoms();
279 let module_name = atoms.intern("liminal_conversation_actor");
280 let entry_function = atoms.intern("main");
281 let command_function = atoms.intern("process_command");
282 let command_atom = atoms.intern("liminal_conversation_command");
283 let participant_wakeup_atom = atoms.intern("liminal_conversation_participant_wakeup");
284 let runtime = Arc::new(ActorRuntime::new(command_atom));
285 let participant_runtime = Arc::new(ParticipantRuntime::default());
286 let registry = Arc::new(ModuleRegistry::new());
287 registry.insert(actor_module(module_name, entry_function, command_function));
288 let private_data: Arc<dyn Any + Send + Sync> = runtime.clone();
289 let scheduler = Scheduler::new(
290 SchedulerConfig {
291 thread_count: Some(1),
292 nif_private_data: Some(private_data),
293 ..SchedulerConfig::default()
294 },
295 registry,
296 NativeBifs::none(),
303 )
304 .map_err(|message| LiminalError::ConversationFailed { message })?;
305 Ok(Self {
306 scheduler: Arc::new(scheduler),
307 runtime,
308 participant_runtime,
309 participant_wakeup_atom,
310 module_name,
311 entry_function,
312 #[cfg(test)]
313 boot_barrier: Mutex::new(None),
314 })
315 }
316
317 #[cfg(test)]
320 fn install_boot_barrier(&self, barrier: BootBarrier) {
321 if let Ok(mut slot) = self.boot_barrier.lock() {
322 *slot = Some(barrier);
323 }
324 }
325
326 fn spawn_participant(&self) -> Result<ParticipantChannel, LiminalError> {
332 let runtime = Arc::clone(&self.participant_runtime);
333 let wakeup_atom = self.participant_wakeup_atom;
334 let factory = Box::new(move || {
335 Box::new(ParticipantProcess::new(Arc::clone(&runtime), wakeup_atom))
336 as Box<dyn beamr::native::native_process::NativeHandler>
337 });
338 let pid = self.scheduler.spawn_native(factory).map_err(|error| {
339 LiminalError::ConversationFailed {
340 message: format!("failed to spawn conversation participant: {error}"),
341 }
342 })?;
343 let participant = ParticipantPid::new(pid);
344 let inbox = Arc::new(Mutex::new(VecDeque::new()));
345 Ok(ParticipantChannel::new(participant, inbox))
346 }
347
348 fn spawn_actor_for(
357 self: &Arc<Self>,
358 core: &Arc<ActorCore>,
359 ) -> Result<ParticipantPid, LiminalError> {
360 if core.is_finalized() {
361 return Err(LiminalError::ConversationFailed {
362 message: "conversation is closed".to_owned(),
363 });
364 }
365 let pid = self
366 .scheduler
367 .spawn_trap_exit(self.module_name, self.entry_function, Vec::new())
368 .map_err(|error| LiminalError::ConversationFailed {
369 message: format!("failed to spawn conversation actor: {error}"),
370 })?;
371 let actor = ParticipantPid::new(pid);
372 if let Err(error) = self.runtime.register(actor, Arc::downgrade(core)) {
373 self.rollback_actor_attempt(core, actor, None);
374 return Err(error);
375 }
376 let watcher = match self.spawn_watcher(core, actor) {
377 Ok(watcher) => watcher,
378 Err(error) => {
380 self.rollback_actor_attempt(core, actor, None);
381 return Err(error);
382 }
383 };
384 if let Err(error) = core
385 .set_watcher_pid(watcher)
386 .and_then(|()| core.set_current_pid(actor))
387 {
388 self.rollback_actor_attempt(core, actor, Some(watcher));
389 return Err(error);
390 }
391 #[cfg(test)]
392 self.boot_barrier_rendezvous(actor);
393 if let Err(error) = core.boot(actor) {
394 self.rollback_actor_attempt(core, actor, Some(watcher));
395 return Err(error);
396 }
397 if core.is_finalized() {
401 self.rollback_actor_attempt(core, actor, Some(watcher));
402 return Err(LiminalError::ConversationFailed {
403 message: "conversation is closed".to_owned(),
404 });
405 }
406 Ok(actor)
407 }
408
409 fn rollback_actor_attempt(
414 &self,
415 core: &ActorCore,
416 actor: ParticipantPid,
417 watcher: Option<ParticipantPid>,
418 ) {
419 if let Some(watcher) = watcher {
420 self.scheduler
421 .terminate_process(watcher.get(), beamr::process::ExitReason::Normal);
422 }
423 self.scheduler
424 .terminate_process(actor.get(), beamr::process::ExitReason::Normal);
425 self.runtime.deregister_owned(actor, core);
426 }
427
428 #[cfg(test)]
434 fn boot_barrier_rendezvous(&self, actor: ParticipantPid) {
435 let barrier = self
436 .boot_barrier
437 .lock()
438 .ok()
439 .and_then(|mut barrier| barrier.take());
440 if let Some((notify, proceed)) = barrier {
441 let _ = notify.send(actor);
442 let _ = proceed.recv();
443 }
444 }
445
446 fn spawn_watcher(
453 self: &Arc<Self>,
454 core: &Arc<ActorCore>,
455 actor: ParticipantPid,
456 ) -> Result<ParticipantPid, LiminalError> {
457 let (armed_tx, armed_rx) = mpsc::sync_channel::<()>(1);
458 let watcher_core = Arc::downgrade(core);
459 let watcher_supervisor = Arc::downgrade(self);
460 let factory = Box::new(move || {
461 Box::new(watcher::ActorExitWatcher::new(
462 watcher_core.clone(),
463 watcher_supervisor.clone(),
464 actor,
465 armed_tx.clone(),
466 )) as Box<dyn beamr::native::native_process::NativeHandler>
467 });
468 let watcher_pid = self.scheduler.spawn_native(factory).map_err(|error| {
469 LiminalError::ConversationFailed {
470 message: format!("failed to spawn conversation exit watcher: {error}"),
471 }
472 })?;
473 if armed_rx
476 .recv_timeout(std::time::Duration::from_secs(5))
477 .is_err()
478 {
479 self.scheduler
480 .terminate_process(watcher_pid, beamr::process::ExitReason::Normal);
481 return Err(LiminalError::ConversationFailed {
482 message: "conversation exit watcher failed to arm".to_owned(),
483 });
484 }
485 Ok(ParticipantPid::new(watcher_pid))
486 }
487}