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
33/// Backs every [`ActorSystem::testing`] scope in the process. Initialised once, never shut down.
34static TESTING_ROOT: OnceLock<ActorSystem> = OnceLock::new();
35
36struct ActorSystemInner {
37	cancel: CancellationToken,
38	scheduler: SchedulerHandle,
39	clock: Clock,
40	pools: Pools,
41	wakers: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>>,
42	keepalive: Mutex<Vec<Box<dyn Any + Send + Sync>>>,
43	done_rxs: Mutex<Vec<Receiver<()>>>,
44	children: Mutex<Vec<ActorSystem>>,
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			}),
67		}
68	}
69
70	/// A scope over one process-wide set of worker threads and timer scheduler, held alive by the shared
71	/// root so callers may drop it while the spawners they handed out keep working. A pool per fixture
72	/// would spawn threads nothing joins and hit the OS per-process thread limit within one test binary.
73	pub fn testing(clock: Clock) -> Self {
74		TESTING_ROOT.get_or_init(|| Self::new(Pools::new(PoolConfig::default()), Clock::Real)).scope_with(clock)
75	}
76
77	pub fn scope(&self) -> Self {
78		self.scope_with(self.inner.clock.clone())
79	}
80
81	fn scope_with(&self, clock: Clock) -> Self {
82		let child = Self {
83			inner: Arc::new(ActorSystemInner {
84				cancel: self.inner.cancel.child_token(),
85				scheduler: self.inner.scheduler.shared(),
86				clock,
87				pools: self.inner.pools.clone(),
88				wakers: Mutex::new(Vec::new()),
89				keepalive: Mutex::new(Vec::new()),
90				done_rxs: Mutex::new(Vec::new()),
91				children: Mutex::new(Vec::new()),
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		{
121			let mut children = self.inner.children.lock();
122			for child in children.iter() {
123				child.shutdown();
124			}
125			children.clear();
126		}
127
128		let wakers = mem::take(&mut *self.inner.wakers.lock());
129		for waker in &wakers {
130			waker();
131		}
132		drop(wakers);
133
134		self.inner.keepalive.lock().clear();
135	}
136
137	pub(crate) fn register_waker(&self, f: Arc<dyn Fn() + Send + Sync>) {
138		self.inner.wakers.lock().push(f);
139	}
140
141	pub(crate) fn register_keepalive(&self, cell: Box<dyn Any + Send + Sync>) {
142		self.inner.keepalive.lock().push(cell);
143	}
144
145	pub(crate) fn register_done_rx(&self, rx: Receiver<()>) {
146		self.inner.done_rxs.lock().push(rx);
147	}
148
149	pub fn join(&self) -> Result<(), JoinError> {
150		self.join_timeout(Duration::from_secs(5))
151	}
152
153	#[allow(clippy::disallowed_methods)]
154	pub fn join_timeout(&self, timeout: Duration) -> Result<(), JoinError> {
155		let deadline = time::Instant::now() + timeout;
156		let rxs: Vec<_> = mem::take(&mut *self.inner.done_rxs.lock());
157		for rx in rxs {
158			let remaining = deadline.saturating_duration_since(time::Instant::now());
159			match rx.recv_timeout(remaining) {
160				Ok(()) => {}
161				Err(CcRecvTimeoutError::Disconnected) => {}
162				Err(CcRecvTimeoutError::Timeout) => {
163					return Err(JoinError::new("timed out waiting for actors to stop"));
164				}
165			}
166		}
167		Ok(())
168	}
169
170	pub fn scheduler(&self) -> &SchedulerHandle {
171		&self.inner.scheduler
172	}
173
174	pub fn clock(&self) -> &Clock {
175		&self.inner.clock
176	}
177
178	pub fn spawn_coordination<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
179	where
180		A::State: Send,
181	{
182		let group = self.inner.pools.actor_pool().coordination();
183		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
184	}
185
186	pub fn spawn_flow<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
187	where
188		A::State: Send,
189	{
190		let group = self.inner.pools.actor_pool().flow();
191		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
192	}
193
194	pub fn spawn_maintenance<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
195	where
196		A::State: Send,
197	{
198		let group = self.inner.pools.actor_pool().maintenance();
199		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
200	}
201
202	pub fn spawn_ephemeral<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
203	where
204		A::State: Send,
205	{
206		pool::spawn_on_schedule(self, name, actor, self.inner.pools.task_injector(), EPHEMERAL_BATCH_SIZE)
207	}
208}
209
210impl Debug for ActorSystem {
211	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
212		f.debug_struct("ActorSystem").field("cancelled", &self.is_cancelled()).finish_non_exhaustive()
213	}
214}
215
216#[derive(Clone)]
217pub struct ActorSpawner {
218	inner: Weak<ActorSystemInner>,
219	clock: Clock,
220}
221
222impl ActorSpawner {
223	fn system(&self) -> ActorSystem {
224		ActorSystem {
225			inner: self.inner.upgrade().expect("runtime already shut down: cannot spawn actor"),
226		}
227	}
228
229	pub fn clock(&self) -> &Clock {
230		&self.clock
231	}
232
233	pub fn pools(&self) -> Pools {
234		self.system().pools()
235	}
236
237	pub fn is_alive(&self) -> bool {
238		self.inner.strong_count() > 0
239	}
240
241	pub fn cancellation_token(&self) -> Option<CancellationToken> {
242		self.inner.upgrade().map(|inner| inner.cancel.clone())
243	}
244
245	pub fn scope(&self) -> ActorSpawner {
246		self.system().scope().spawner()
247	}
248
249	pub fn shutdown(&self) {
250		if let Some(inner) = self.inner.upgrade() {
251			ActorSystem {
252				inner,
253			}
254			.shutdown();
255		}
256	}
257
258	pub fn spawn_coordination<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
259	where
260		A::State: Send,
261	{
262		self.system().spawn_coordination(name, actor)
263	}
264
265	pub fn spawn_flow<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
266	where
267		A::State: Send,
268	{
269		self.system().spawn_flow(name, actor)
270	}
271
272	pub fn spawn_maintenance<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
273	where
274		A::State: Send,
275	{
276		self.system().spawn_maintenance(name, actor)
277	}
278
279	pub fn spawn_ephemeral<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
280	where
281		A::State: Send,
282	{
283		self.system().spawn_ephemeral(name, actor)
284	}
285}
286
287impl Debug for ActorSpawner {
288	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
289		f.debug_struct("ActorSpawner").field("alive", &self.is_alive()).finish_non_exhaustive()
290	}
291}
292
293pub type ActorHandle<M> = PoolActorHandle<M>;
294
295#[derive(Debug)]
296pub struct JoinError {
297	message: String,
298}
299
300impl JoinError {
301	pub fn new(message: impl Into<String>) -> Self {
302		Self {
303			message: message.into(),
304		}
305	}
306}
307
308impl fmt::Display for JoinError {
309	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
310		write!(f, "actor join failed: {}", self.message)
311	}
312}
313
314impl error::Error for JoinError {}
315
316#[cfg(test)]
317mod tests {
318	use std::sync;
319
320	use super::*;
321	use crate::{
322		actor::{context::Context, traits::Directive},
323		pool::{PoolConfig, Pools},
324	};
325
326	fn test_system() -> ActorSystem {
327		let pools = Pools::new(PoolConfig::default());
328		ActorSystem::new(pools, Clock::Real)
329	}
330
331	struct CounterActor;
332
333	#[derive(Debug)]
334	enum CounterMessage {
335		Inc,
336		Get(sync::mpsc::Sender<i64>),
337		Stop,
338	}
339
340	impl Actor for CounterActor {
341		type State = i64;
342		type Message = CounterMessage;
343
344		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {
345			0
346		}
347
348		fn handle(
349			&self,
350			state: &mut Self::State,
351			msg: Self::Message,
352			_ctx: &Context<Self::Message>,
353		) -> Directive {
354			match msg {
355				CounterMessage::Inc => *state += 1,
356				CounterMessage::Get(tx) => {
357					let _ = tx.send(*state);
358				}
359				CounterMessage::Stop => return Directive::Stop,
360			}
361			Directive::Continue
362		}
363	}
364
365	#[test]
366	fn test_spawn_and_send() {
367		let system = test_system();
368		let handle = system.spawn_coordination("counter", CounterActor);
369
370		let actor_ref = handle.actor_ref().clone();
371		actor_ref.send(CounterMessage::Inc).unwrap();
372		actor_ref.send(CounterMessage::Inc).unwrap();
373		actor_ref.send(CounterMessage::Inc).unwrap();
374
375		let (tx, rx) = sync::mpsc::channel();
376		actor_ref.send(CounterMessage::Get(tx)).unwrap();
377
378		let value = rx.recv().unwrap();
379		assert_eq!(value, 3);
380
381		actor_ref.send(CounterMessage::Stop).unwrap();
382		handle.join().unwrap();
383	}
384
385	#[test]
386	fn test_shutdown_join() {
387		let system = test_system();
388
389		for i in 0..5 {
390			system.spawn_coordination(&format!("counter-{i}"), CounterActor);
391		}
392
393		// join() must not return before every actor has finished, or shutdown races teardown.
394		system.shutdown();
395		system.join().unwrap();
396	}
397}