Skip to main content

reifydb_runtime/actor/system/native/
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, Weak},
14	time,
15	time::Duration,
16};
17
18use crossbeam_channel::{Receiver, RecvTimeoutError as CcRecvTimeoutError};
19
20use crate::{
21	actor::{
22		context::CancellationToken, system::native::pool::PoolActorHandle, timers::scheduler::SchedulerHandle,
23		traits::Actor,
24	},
25	context::clock::Clock,
26	pool::{
27		Pools,
28		actor_pool::{EPHEMERAL_BATCH_SIZE, Schedule},
29	},
30	sync::mutex::Mutex,
31};
32
33struct ActorSystemInner {
34	cancel: CancellationToken,
35	scheduler: SchedulerHandle,
36	clock: Clock,
37	pools: Pools,
38	wakers: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>>,
39	keepalive: Mutex<Vec<Box<dyn Any + Send + Sync>>>,
40	done_rxs: Mutex<Vec<Receiver<()>>>,
41	children: Mutex<Vec<ActorSystem>>,
42}
43
44#[derive(Clone)]
45pub struct ActorSystem {
46	inner: Arc<ActorSystemInner>,
47}
48
49impl ActorSystem {
50	pub fn new(pools: Pools, clock: Clock) -> Self {
51		let scheduler = SchedulerHandle::new();
52
53		Self {
54			inner: Arc::new(ActorSystemInner {
55				cancel: CancellationToken::new(),
56				scheduler,
57				clock,
58				pools,
59				wakers: Mutex::new(Vec::new()),
60				keepalive: Mutex::new(Vec::new()),
61				done_rxs: Mutex::new(Vec::new()),
62				children: Mutex::new(Vec::new()),
63			}),
64		}
65	}
66
67	pub fn scope(&self) -> Self {
68		let child = Self {
69			inner: Arc::new(ActorSystemInner {
70				cancel: self.inner.cancel.child_token(),
71				scheduler: self.inner.scheduler.shared(),
72				clock: self.inner.clock.clone(),
73				pools: self.inner.pools.clone(),
74				wakers: Mutex::new(Vec::new()),
75				keepalive: Mutex::new(Vec::new()),
76				done_rxs: Mutex::new(Vec::new()),
77				children: Mutex::new(Vec::new()),
78			}),
79		};
80		self.inner.children.lock().push(child.clone());
81		child
82	}
83
84	pub fn pools(&self) -> Pools {
85		self.inner.pools.clone()
86	}
87
88	pub fn spawner(&self) -> ActorSpawner {
89		ActorSpawner {
90			inner: Arc::downgrade(&self.inner),
91			clock: self.inner.clock.clone(),
92		}
93	}
94
95	pub fn cancellation_token(&self) -> CancellationToken {
96		self.inner.cancel.clone()
97	}
98
99	pub fn is_cancelled(&self) -> bool {
100		self.inner.cancel.is_cancelled()
101	}
102
103	pub fn shutdown(&self) {
104		self.inner.cancel.cancel();
105
106		{
107			let mut children = self.inner.children.lock();
108			for child in children.iter() {
109				child.shutdown();
110			}
111			children.clear();
112		}
113
114		let wakers = mem::take(&mut *self.inner.wakers.lock());
115		for waker in &wakers {
116			waker();
117		}
118		drop(wakers);
119
120		self.inner.keepalive.lock().clear();
121	}
122
123	pub(crate) fn register_waker(&self, f: Arc<dyn Fn() + Send + Sync>) {
124		self.inner.wakers.lock().push(f);
125	}
126
127	pub(crate) fn register_keepalive(&self, cell: Box<dyn Any + Send + Sync>) {
128		self.inner.keepalive.lock().push(cell);
129	}
130
131	pub(crate) fn register_done_rx(&self, rx: Receiver<()>) {
132		self.inner.done_rxs.lock().push(rx);
133	}
134
135	pub fn join(&self) -> Result<(), JoinError> {
136		self.join_timeout(Duration::from_secs(5))
137	}
138
139	#[allow(clippy::disallowed_methods)]
140	pub fn join_timeout(&self, timeout: Duration) -> Result<(), JoinError> {
141		let deadline = time::Instant::now() + timeout;
142		let rxs: Vec<_> = mem::take(&mut *self.inner.done_rxs.lock());
143		for rx in rxs {
144			let remaining = deadline.saturating_duration_since(time::Instant::now());
145			match rx.recv_timeout(remaining) {
146				Ok(()) => {}
147				Err(CcRecvTimeoutError::Disconnected) => {}
148				Err(CcRecvTimeoutError::Timeout) => {
149					return Err(JoinError::new("timed out waiting for actors to stop"));
150				}
151			}
152		}
153		Ok(())
154	}
155
156	pub fn scheduler(&self) -> &SchedulerHandle {
157		&self.inner.scheduler
158	}
159
160	pub fn clock(&self) -> &Clock {
161		&self.inner.clock
162	}
163
164	pub fn spawn_coordination<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
165	where
166		A::State: Send,
167	{
168		let group = self.inner.pools.actor_pool().coordination();
169		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
170	}
171
172	pub fn spawn_flow<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
173	where
174		A::State: Send,
175	{
176		let group = self.inner.pools.actor_pool().flow();
177		pool::spawn_on_schedule(self, name, actor, Schedule::Pinned(group.assign()), group.batch_size())
178	}
179
180	pub fn spawn_ephemeral<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
181	where
182		A::State: Send,
183	{
184		pool::spawn_on_schedule(self, name, actor, self.inner.pools.task_injector(), EPHEMERAL_BATCH_SIZE)
185	}
186}
187
188impl Debug for ActorSystem {
189	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
190		f.debug_struct("ActorSystem").field("cancelled", &self.is_cancelled()).finish_non_exhaustive()
191	}
192}
193
194#[derive(Clone)]
195pub struct ActorSpawner {
196	inner: Weak<ActorSystemInner>,
197	clock: Clock,
198}
199
200impl ActorSpawner {
201	fn system(&self) -> ActorSystem {
202		ActorSystem {
203			inner: self.inner.upgrade().expect("runtime already shut down: cannot spawn actor"),
204		}
205	}
206
207	pub fn clock(&self) -> &Clock {
208		&self.clock
209	}
210
211	pub fn pools(&self) -> Pools {
212		self.system().pools()
213	}
214
215	pub fn is_alive(&self) -> bool {
216		self.inner.strong_count() > 0
217	}
218
219	pub fn cancellation_token(&self) -> Option<CancellationToken> {
220		self.inner.upgrade().map(|inner| inner.cancel.clone())
221	}
222
223	pub fn scope(&self) -> ActorSpawner {
224		self.system().scope().spawner()
225	}
226
227	pub fn shutdown(&self) {
228		if let Some(inner) = self.inner.upgrade() {
229			ActorSystem {
230				inner,
231			}
232			.shutdown();
233		}
234	}
235
236	pub fn spawn_coordination<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
237	where
238		A::State: Send,
239	{
240		self.system().spawn_coordination(name, actor)
241	}
242
243	pub fn spawn_flow<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
244	where
245		A::State: Send,
246	{
247		self.system().spawn_flow(name, actor)
248	}
249
250	pub fn spawn_ephemeral<A: Actor>(&self, name: &str, actor: A) -> ActorHandle<A::Message>
251	where
252		A::State: Send,
253	{
254		self.system().spawn_ephemeral(name, actor)
255	}
256}
257
258impl Debug for ActorSpawner {
259	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
260		f.debug_struct("ActorSpawner").field("alive", &self.is_alive()).finish_non_exhaustive()
261	}
262}
263
264pub type ActorHandle<M> = PoolActorHandle<M>;
265
266#[derive(Debug)]
267pub struct JoinError {
268	message: String,
269}
270
271impl JoinError {
272	pub fn new(message: impl Into<String>) -> Self {
273		Self {
274			message: message.into(),
275		}
276	}
277}
278
279impl fmt::Display for JoinError {
280	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
281		write!(f, "actor join failed: {}", self.message)
282	}
283}
284
285impl error::Error for JoinError {}
286
287#[cfg(test)]
288mod tests {
289	use std::sync;
290
291	use super::*;
292	use crate::{
293		actor::{context::Context, traits::Directive},
294		pool::{PoolConfig, Pools},
295	};
296
297	fn test_system() -> ActorSystem {
298		let pools = Pools::new(PoolConfig::default());
299		ActorSystem::new(pools, Clock::Real)
300	}
301
302	struct CounterActor;
303
304	#[derive(Debug)]
305	enum CounterMessage {
306		Inc,
307		Get(sync::mpsc::Sender<i64>),
308		Stop,
309	}
310
311	impl Actor for CounterActor {
312		type State = i64;
313		type Message = CounterMessage;
314
315		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {
316			0
317		}
318
319		fn handle(
320			&self,
321			state: &mut Self::State,
322			msg: Self::Message,
323			_ctx: &Context<Self::Message>,
324		) -> Directive {
325			match msg {
326				CounterMessage::Inc => *state += 1,
327				CounterMessage::Get(tx) => {
328					let _ = tx.send(*state);
329				}
330				CounterMessage::Stop => return Directive::Stop,
331			}
332			Directive::Continue
333		}
334	}
335
336	#[test]
337	fn test_spawn_and_send() {
338		let system = test_system();
339		let handle = system.spawn_coordination("counter", CounterActor);
340
341		let actor_ref = handle.actor_ref().clone();
342		actor_ref.send(CounterMessage::Inc).unwrap();
343		actor_ref.send(CounterMessage::Inc).unwrap();
344		actor_ref.send(CounterMessage::Inc).unwrap();
345
346		let (tx, rx) = sync::mpsc::channel();
347		actor_ref.send(CounterMessage::Get(tx)).unwrap();
348
349		let value = rx.recv().unwrap();
350		assert_eq!(value, 3);
351
352		actor_ref.send(CounterMessage::Stop).unwrap();
353		handle.join().unwrap();
354	}
355
356	#[test]
357	fn test_shutdown_join() {
358		let system = test_system();
359
360		// Spawn several actors
361		for i in 0..5 {
362			system.spawn_coordination(&format!("counter-{i}"), CounterActor);
363		}
364
365		// Shutdown cancels all actors; join waits for them to finish
366		system.shutdown();
367		system.join().unwrap();
368	}
369}