Skip to main content

reifydb_runtime/actor/system/host/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4#![allow(clippy::disallowed_types)]
5
6mod pool;
7
8use std::{
9	any::Any,
10	error, fmt,
11	fmt::{Debug, Formatter},
12	mem,
13	sync::{Arc, OnceLock, Weak},
14	time,
15	time::Duration,
16};
17
18use crossbeam_channel::{Receiver, RecvTimeoutError as CcRecvTimeoutError};
19
20use crate::{
21	actor::{
22		context::CancellationToken, system::host::pool::PoolActorHandle, timers::scheduler::SchedulerHandle,
23		traits::Actor,
24	},
25	context::clock::Clock,
26	pool::{
27		PoolConfig, Pools,
28		actor_pool::{EPHEMERAL_BATCH_SIZE, Schedule},
29	},
30	sync::mutex::Mutex,
31};
32
33static TESTING_ROOT: OnceLock<ActorSystem> = OnceLock::new();
34
35struct ActorSystemInner {
36	cancel: CancellationToken,
37	scheduler: SchedulerHandle,
38	clock: Clock,
39	pools: Pools,
40	wakers: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>>,
41	keepalive: Mutex<Vec<Box<dyn Any + Send + Sync>>>,
42	done_rxs: Mutex<Vec<Receiver<Result<(), String>>>>,
43	children: Mutex<Vec<ActorSystem>>,
44	parent: Weak<ActorSystemInner>,
45}
46
47#[derive(Clone)]
48pub struct ActorSystem {
49	inner: Arc<ActorSystemInner>,
50}
51
52impl ActorSystem {
53	pub fn new(pools: Pools, clock: Clock) -> Self {
54		let scheduler = SchedulerHandle::new();
55
56		Self {
57			inner: Arc::new(ActorSystemInner {
58				cancel: CancellationToken::new(),
59				scheduler,
60				clock,
61				pools,
62				wakers: Mutex::new(Vec::new()),
63				keepalive: Mutex::new(Vec::new()),
64				done_rxs: Mutex::new(Vec::new()),
65				children: Mutex::new(Vec::new()),
66				parent: Weak::new(),
67			}),
68		}
69	}
70
71	pub fn testing(clock: Clock) -> Self {
72		TESTING_ROOT.get_or_init(|| Self::new(Pools::new(PoolConfig::default()), Clock::Real)).scope_with(clock)
73	}
74
75	pub fn scope(&self) -> Self {
76		self.scope_with(self.inner.clock.clone())
77	}
78
79	fn scope_with(&self, clock: Clock) -> Self {
80		let cancel = self.inner.cancel.child_token();
81		let child = Self {
82			inner: Arc::new(ActorSystemInner {
83				scheduler: self.inner.scheduler.shared(cancel.clone()),
84				cancel,
85				clock,
86				pools: self.inner.pools.clone(),
87				wakers: Mutex::new(Vec::new()),
88				keepalive: Mutex::new(Vec::new()),
89				done_rxs: Mutex::new(Vec::new()),
90				children: Mutex::new(Vec::new()),
91				parent: Arc::downgrade(&self.inner),
92			}),
93		};
94		self.inner.children.lock().push(child.clone());
95		child
96	}
97
98	pub fn pools(&self) -> Pools {
99		self.inner.pools.clone()
100	}
101
102	pub fn spawner(&self) -> ActorSpawner {
103		ActorSpawner {
104			inner: Arc::downgrade(&self.inner),
105			clock: self.inner.clock.clone(),
106		}
107	}
108
109	pub fn cancellation_token(&self) -> CancellationToken {
110		self.inner.cancel.clone()
111	}
112
113	pub fn is_cancelled(&self) -> bool {
114		self.inner.cancel.is_cancelled()
115	}
116
117	pub fn shutdown(&self) {
118		self.inner.cancel.cancel();
119
120		let children = mem::take(&mut *self.inner.children.lock());
121		for child in &children {
122			child.shutdown();
123		}
124		drop(children);
125
126		let wakers = mem::take(&mut *self.inner.wakers.lock());
127		for waker in &wakers {
128			waker();
129		}
130		drop(wakers);
131
132		self.inner.scheduler.shutdown();
133
134		self.inner.keepalive.lock().clear();
135
136		if let Some(parent) = self.inner.parent.upgrade() {
137			parent.children.lock().retain(|child| !Arc::ptr_eq(&child.inner, &self.inner));
138		}
139	}
140
141	pub(crate) fn register_waker(&self, f: Arc<dyn Fn() + Send + Sync>) {
142		self.inner.wakers.lock().push(f);
143	}
144
145	pub(crate) fn register_keepalive(&self, cell: Box<dyn Any + Send + Sync>) {
146		self.inner.keepalive.lock().push(cell);
147	}
148
149	pub(crate) fn register_done_rx(&self, rx: Receiver<Result<(), String>>) {
150		self.inner.done_rxs.lock().push(rx);
151	}
152
153	pub fn join(&self) -> Result<(), JoinError> {
154		self.join_timeout(Duration::from_secs(5))
155	}
156
157	#[allow(clippy::disallowed_methods)]
158	pub fn join_timeout(&self, timeout: Duration) -> Result<(), JoinError> {
159		let deadline = time::Instant::now() + timeout;
160		let rxs: Vec<_> = mem::take(&mut *self.inner.done_rxs.lock());
161		for rx in rxs {
162			let remaining = deadline.saturating_duration_since(time::Instant::now());
163			match rx.recv_timeout(remaining) {
164				Ok(Ok(())) => {}
165				Ok(Err(name)) => {
166					return Err(JoinError::new(format!("actor {name} panicked")));
167				}
168				Err(CcRecvTimeoutError::Disconnected) => {}
169				Err(CcRecvTimeoutError::Timeout) => {
170					return Err(JoinError::new("timed out waiting for actors to stop"));
171				}
172			}
173		}
174		Ok(())
175	}
176
177	pub fn scheduler(&self) -> &SchedulerHandle {
178		&self.inner.scheduler
179	}
180
181	pub fn clock(&self) -> &Clock {
182		&self.inner.clock
183	}
184
185	pub fn spawn_coordination<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
186	where
187		A::State: Send,
188	{
189		let group = self.inner.pools.actor_pool().coordination();
190		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
191	}
192
193	pub fn spawn_flow<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
194	where
195		A::State: Send,
196	{
197		let group = self.inner.pools.actor_pool().flow();
198		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
199	}
200
201	pub fn spawn_maintenance<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
202	where
203		A::State: Send,
204	{
205		let group = self.inner.pools.actor_pool().maintenance();
206		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
207	}
208
209	pub fn spawn_ephemeral<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
210	where
211		A::State: Send,
212	{
213		pool::spawn_on_schedule(self, name, actor, self.inner.pools.task_injector(), EPHEMERAL_BATCH_SIZE)
214	}
215}
216
217impl Debug for ActorSystem {
218	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
219		f.debug_struct("ActorSystem").field("cancelled", &self.is_cancelled()).finish_non_exhaustive()
220	}
221}
222
223#[derive(Clone)]
224pub struct ActorSpawner {
225	inner: Weak<ActorSystemInner>,
226	clock: Clock,
227}
228
229impl ActorSpawner {
230	fn system(&self) -> ActorSystem {
231		ActorSystem {
232			inner: self.inner.upgrade().expect("runtime already shut down: cannot spawn actor"),
233		}
234	}
235
236	pub fn clock(&self) -> &Clock {
237		&self.clock
238	}
239
240	pub fn pools(&self) -> Pools {
241		self.system().pools()
242	}
243
244	pub fn is_alive(&self) -> bool {
245		self.inner.strong_count() > 0
246	}
247
248	pub fn cancellation_token(&self) -> Option<CancellationToken> {
249		self.inner.upgrade().map(|inner| inner.cancel.clone())
250	}
251
252	pub fn scope(&self) -> ActorSpawner {
253		self.system().scope().spawner()
254	}
255
256	pub fn shutdown(&self) {
257		if let Some(inner) = self.inner.upgrade() {
258			ActorSystem {
259				inner,
260			}
261			.shutdown();
262		}
263	}
264
265	pub fn spawn_coordination<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
266	where
267		A::State: Send,
268	{
269		self.system().spawn_coordination(name, actor)
270	}
271
272	pub fn spawn_flow<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
273	where
274		A::State: Send,
275	{
276		self.system().spawn_flow(name, actor)
277	}
278
279	pub fn spawn_maintenance<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
280	where
281		A::State: Send,
282	{
283		self.system().spawn_maintenance(name, actor)
284	}
285
286	pub fn spawn_ephemeral<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
287	where
288		A::State: Send,
289	{
290		self.system().spawn_ephemeral(name, actor)
291	}
292}
293
294impl Debug for ActorSpawner {
295	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
296		f.debug_struct("ActorSpawner").field("alive", &self.is_alive()).finish_non_exhaustive()
297	}
298}
299
300pub type ActorHandle<M> = PoolActorHandle<M>;
301
302#[derive(Debug)]
303pub struct JoinError {
304	message: String,
305}
306
307impl JoinError {
308	pub fn new(message: impl Into<String>) -> Self {
309		Self {
310			message: message.into(),
311		}
312	}
313}
314
315impl fmt::Display for JoinError {
316	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
317		write!(f, "actor join failed: {}", self.message)
318	}
319}
320
321impl error::Error for JoinError {}
322
323#[cfg(test)]
324mod tests {
325	use std::sync;
326
327	use super::*;
328	use crate::{
329		actor::{context::Context, mailbox::ActorRef, system::ActorConfig, traits::Directive},
330		pool::{PoolConfig, Pools},
331	};
332
333	fn test_system() -> ActorSystem {
334		let pools = Pools::new(PoolConfig::default());
335		ActorSystem::new(pools, Clock::Real)
336	}
337
338	struct CounterActor;
339
340	#[derive(Debug)]
341	enum CounterMessage {
342		Inc,
343		Get(sync::mpsc::Sender<i64>),
344		Stop,
345	}
346
347	impl Actor for CounterActor {
348		type State = i64;
349		type Message = CounterMessage;
350
351		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {
352			0
353		}
354
355		fn handle(
356			&self,
357			state: &mut Self::State,
358			msg: Self::Message,
359			_ctx: &Context<Self::Message>,
360		) -> Directive {
361			match msg {
362				CounterMessage::Inc => *state += 1,
363				CounterMessage::Get(tx) => {
364					let _ = tx.send(*state);
365				}
366				CounterMessage::Stop => return Directive::Stop,
367			}
368			Directive::Continue
369		}
370	}
371
372	#[test]
373	fn test_spawn_and_send() {
374		let system = test_system();
375		let handle = system.spawn_coordination("counter", CounterActor);
376
377		let actor_ref = handle.actor_ref().clone();
378		actor_ref.send(CounterMessage::Inc).unwrap();
379		actor_ref.send(CounterMessage::Inc).unwrap();
380		actor_ref.send(CounterMessage::Inc).unwrap();
381
382		let (tx, rx) = sync::mpsc::channel();
383		actor_ref.send(CounterMessage::Get(tx)).unwrap();
384
385		let value = rx.recv().unwrap();
386		assert_eq!(value, 3);
387
388		actor_ref.send(CounterMessage::Stop).unwrap();
389		handle.join().unwrap();
390	}
391
392	#[test]
393	fn test_shutdown_join() {
394		let system = test_system();
395
396		for i in 0..5 {
397			system.spawn_coordination(&format!("counter-{i}"), CounterActor);
398		}
399
400		// join() must not return before every actor has finished, or shutdown races teardown.
401		system.shutdown();
402		system.join().unwrap();
403	}
404
405	#[test]
406	fn test_shutdown_stops_the_timer_scheduler() {
407		// Timers must be dead before the cells are released, otherwise a callback owns the last system ref.
408		let system = test_system();
409		system.shutdown();
410
411		let (tx, rx) = sync::mpsc::channel();
412		system.scheduler().schedule_once(Duration::from_millis(5), move || {
413			let _ = tx.send(());
414		});
415
416		assert!(rx.recv_timeout(Duration::from_millis(200)).is_err());
417	}
418
419	#[test]
420	fn a_scope_that_shuts_down_leaves_its_parent_but_a_live_scope_still_stops_with_it() {
421		// A parent that keeps every stopped scope grows by one system per open and close, forever.
422		let root = test_system();
423		let stopped = root.scope();
424		let live = root.scope();
425		assert_eq!(root.inner.children.lock().len(), 2, "precondition: both scopes are registered");
426
427		stopped.shutdown();
428		let children = root.inner.children.lock().clone();
429		assert_eq!(children.len(), 1, "a stopped scope must leave its parent's list");
430		assert!(Arc::ptr_eq(&children[0].inner, &live.inner), "only the stopped scope may leave");
431		drop(children);
432
433		root.shutdown();
434		assert!(live.is_cancelled(), "a parent shutdown must still stop every live scope");
435		assert!(root.inner.children.lock().is_empty(), "the parent must hold no scope after its shutdown");
436	}
437	struct TurnActor {
438		log: Arc<sync::Mutex<Vec<&'static str>>>,
439		name: &'static str,
440		batch: Option<usize>,
441	}
442
443	#[derive(Debug)]
444	enum TurnMessage {
445		Hold(sync::mpsc::Sender<()>, sync::mpsc::Receiver<()>),
446		Kick(ActorRef<TurnMessage>),
447		Log,
448		Done(sync::mpsc::Sender<()>),
449	}
450
451	impl Actor for TurnActor {
452		type State = ();
453		type Message = TurnMessage;
454
455		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {}
456
457		fn handle(
458			&self,
459			_state: &mut Self::State,
460			msg: Self::Message,
461			_ctx: &Context<Self::Message>,
462		) -> Directive {
463			match msg {
464				TurnMessage::Hold(started, release) => {
465					let _ = started.send(());
466					let _ = release.recv();
467				}
468				TurnMessage::Kick(other) => {
469					let _ = other.send(TurnMessage::Log);
470				}
471				TurnMessage::Log => self.log.lock().unwrap().push(self.name),
472				TurnMessage::Done(tx) => {
473					let _ = tx.send(());
474				}
475			}
476			Directive::Continue
477		}
478
479		fn config(&self) -> ActorConfig {
480			match self.batch {
481				Some(batch) => ActorConfig::new().batch_size(batch),
482				None => ActorConfig::new(),
483			}
484		}
485	}
486
487	fn turn_order(batch: Option<usize>) -> Vec<&'static str> {
488		let system = ActorSystem::new(
489			Pools::new(PoolConfig {
490				flow_threads: 1,
491				..PoolConfig::default()
492			}),
493			Clock::Real,
494		);
495		let log = Arc::new(sync::Mutex::new(Vec::new()));
496		let actor = |name, batch| TurnActor {
497			log: Arc::clone(&log),
498			name,
499			batch,
500		};
501		let holder = system.spawn_flow("holder", actor("holder", None));
502		let busy = system.spawn_flow("busy", actor("busy", batch));
503		let other = system.spawn_flow("other", actor("other", None));
504
505		for handle in [&busy, &other] {
506			let (tx, rx) = sync::mpsc::channel();
507			handle.actor_ref().send(TurnMessage::Done(tx)).unwrap();
508			rx.recv().unwrap();
509		}
510
511		let (started_tx, started_rx) = sync::mpsc::channel();
512		let (release_tx, release_rx) = sync::mpsc::channel();
513		holder.actor_ref().send(TurnMessage::Hold(started_tx, release_rx)).unwrap();
514		started_rx.recv().unwrap();
515
516		busy.actor_ref().send(TurnMessage::Kick(other.actor_ref().clone())).unwrap();
517		for _ in 0..20 {
518			busy.actor_ref().send(TurnMessage::Log).unwrap();
519		}
520		let (busy_done_tx, busy_done_rx) = sync::mpsc::channel();
521		busy.actor_ref().send(TurnMessage::Done(busy_done_tx)).unwrap();
522		release_tx.send(()).unwrap();
523		busy_done_rx.recv().unwrap();
524
525		let (other_done_tx, other_done_rx) = sync::mpsc::channel();
526		other.actor_ref().send(TurnMessage::Done(other_done_tx)).unwrap();
527		other_done_rx.recv().unwrap();
528
529		system.shutdown();
530		system.join().unwrap();
531		log.lock().unwrap().clone()
532	}
533
534	#[test]
535	fn an_actor_batch_size_holds_its_turn_for_that_many_messages() {
536		// A shared queue that yields after the pool default makes every sender wait behind the whole pool.
537		let own = turn_order(Some(64));
538		assert_eq!(
539			own.iter().position(|name| *name == "other"),
540			Some(20),
541			"a batch of 64 must finish all 20 queued messages before a newly woken actor runs: {own:?}"
542		);
543
544		let pooled = turn_order(None);
545		assert_eq!(
546			pooled.iter().position(|name| *name == "other"),
547			Some(7),
548			"without its own batch size the actor must yield after the flow pool default of 8: {pooled:?}"
549		);
550	}
551
552	struct PanicActor;
553
554	impl Actor for PanicActor {
555		type State = ();
556		type Message = sync::mpsc::Sender<()>;
557
558		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {}
559
560		fn handle(
561			&self,
562			_state: &mut Self::State,
563			reached: Self::Message,
564			_ctx: &Context<Self::Message>,
565		) -> Directive {
566			let _ = reached.send(());
567			panic!("a pool actor crashed while handling a message");
568		}
569	}
570
571	#[test]
572	fn a_pool_actor_that_panics_fails_the_join_instead_of_passing_shutdown_as_clean() {
573		// A join that reads a dropped done signal as a clean stop hides a crashed actor behind a clean
574		// shutdown.
575		let system = test_system();
576		let crashed = system.spawn_flow("crashed", PanicActor);
577		let (reached_tx, reached_rx) = sync::mpsc::channel();
578		crashed.actor_ref().send(reached_tx).unwrap();
579		reached_rx.recv().unwrap();
580
581		system.shutdown();
582
583		assert!(system.join().is_err(), "a crashed pool actor must fail the join");
584	}
585
586	#[test]
587	fn a_pool_actor_that_panics_fails_its_handle_join_after_shutdown() {
588		// Subsystems join each actor handle on shutdown; a crashed actor read as joined would report a clean
589		// stop.
590		let system = test_system();
591		let crashed = system.spawn_flow("crashed", PanicActor);
592		let (reached_tx, reached_rx) = sync::mpsc::channel();
593		crashed.actor_ref().send(reached_tx).unwrap();
594		reached_rx.recv().unwrap();
595
596		system.shutdown();
597
598		assert!(crashed.join().is_err(), "a crashed pool actor must fail its handle join");
599	}
600}