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, 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}