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::{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 )
297 .map_err(|message| LiminalError::ConversationFailed { message })?;
298 Ok(Self {
299 scheduler: Arc::new(scheduler),
300 runtime,
301 participant_runtime,
302 participant_wakeup_atom,
303 module_name,
304 entry_function,
305 #[cfg(test)]
306 boot_barrier: Mutex::new(None),
307 })
308 }
309
310 #[cfg(test)]
313 fn install_boot_barrier(&self, barrier: BootBarrier) {
314 if let Ok(mut slot) = self.boot_barrier.lock() {
315 *slot = Some(barrier);
316 }
317 }
318
319 fn spawn_participant(&self) -> Result<ParticipantChannel, LiminalError> {
325 let runtime = Arc::clone(&self.participant_runtime);
326 let wakeup_atom = self.participant_wakeup_atom;
327 let factory = Box::new(move || {
328 Box::new(ParticipantProcess::new(Arc::clone(&runtime), wakeup_atom))
329 as Box<dyn beamr::native::native_process::NativeHandler>
330 });
331 let pid = self.scheduler.spawn_native(factory).map_err(|error| {
332 LiminalError::ConversationFailed {
333 message: format!("failed to spawn conversation participant: {error}"),
334 }
335 })?;
336 let participant = ParticipantPid::new(pid);
337 let inbox = Arc::new(Mutex::new(VecDeque::new()));
338 Ok(ParticipantChannel::new(participant, inbox))
339 }
340
341 fn spawn_actor_for(
350 self: &Arc<Self>,
351 core: &Arc<ActorCore>,
352 ) -> Result<ParticipantPid, LiminalError> {
353 if core.is_finalized() {
354 return Err(LiminalError::ConversationFailed {
355 message: "conversation is closed".to_owned(),
356 });
357 }
358 let pid = self
359 .scheduler
360 .spawn_trap_exit(self.module_name, self.entry_function, Vec::new())
361 .map_err(|error| LiminalError::ConversationFailed {
362 message: format!("failed to spawn conversation actor: {error}"),
363 })?;
364 let actor = ParticipantPid::new(pid);
365 if let Err(error) = self.runtime.register(actor, Arc::downgrade(core)) {
366 self.rollback_actor_attempt(core, actor, None);
367 return Err(error);
368 }
369 let watcher = match self.spawn_watcher(core, actor) {
370 Ok(watcher) => watcher,
371 Err(error) => {
373 self.rollback_actor_attempt(core, actor, None);
374 return Err(error);
375 }
376 };
377 if let Err(error) = core
378 .set_watcher_pid(watcher)
379 .and_then(|()| core.set_current_pid(actor))
380 {
381 self.rollback_actor_attempt(core, actor, Some(watcher));
382 return Err(error);
383 }
384 #[cfg(test)]
385 self.boot_barrier_rendezvous(actor);
386 if let Err(error) = core.boot(actor) {
387 self.rollback_actor_attempt(core, actor, Some(watcher));
388 return Err(error);
389 }
390 if core.is_finalized() {
394 self.rollback_actor_attempt(core, actor, Some(watcher));
395 return Err(LiminalError::ConversationFailed {
396 message: "conversation is closed".to_owned(),
397 });
398 }
399 Ok(actor)
400 }
401
402 fn rollback_actor_attempt(
407 &self,
408 core: &ActorCore,
409 actor: ParticipantPid,
410 watcher: Option<ParticipantPid>,
411 ) {
412 if let Some(watcher) = watcher {
413 self.scheduler
414 .terminate_process(watcher.get(), beamr::process::ExitReason::Normal);
415 }
416 self.scheduler
417 .terminate_process(actor.get(), beamr::process::ExitReason::Normal);
418 self.runtime.deregister_owned(actor, core);
419 }
420
421 #[cfg(test)]
427 fn boot_barrier_rendezvous(&self, actor: ParticipantPid) {
428 let barrier = self
429 .boot_barrier
430 .lock()
431 .ok()
432 .and_then(|mut barrier| barrier.take());
433 if let Some((notify, proceed)) = barrier {
434 let _ = notify.send(actor);
435 let _ = proceed.recv();
436 }
437 }
438
439 fn spawn_watcher(
446 self: &Arc<Self>,
447 core: &Arc<ActorCore>,
448 actor: ParticipantPid,
449 ) -> Result<ParticipantPid, LiminalError> {
450 let (armed_tx, armed_rx) = mpsc::sync_channel::<()>(1);
451 let watcher_core = Arc::downgrade(core);
452 let watcher_supervisor = Arc::downgrade(self);
453 let factory = Box::new(move || {
454 Box::new(watcher::ActorExitWatcher::new(
455 watcher_core.clone(),
456 watcher_supervisor.clone(),
457 actor,
458 armed_tx.clone(),
459 )) as Box<dyn beamr::native::native_process::NativeHandler>
460 });
461 let watcher_pid = self.scheduler.spawn_native(factory).map_err(|error| {
462 LiminalError::ConversationFailed {
463 message: format!("failed to spawn conversation exit watcher: {error}"),
464 }
465 })?;
466 if armed_rx
469 .recv_timeout(std::time::Duration::from_secs(5))
470 .is_err()
471 {
472 self.scheduler
473 .terminate_process(watcher_pid, beamr::process::ExitReason::Normal);
474 return Err(LiminalError::ConversationFailed {
475 message: "conversation exit watcher failed to arm".to_owned(),
476 });
477 }
478 Ok(ParticipantPid::new(watcher_pid))
479 }
480}