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