libdd_shared_runtime/shared_runtime/
basic.rs1use super::pausable_worker::{tokio_spawn_fn, PausableWorker};
5use super::{
6 BlockingRuntime, BoxedWorker, SharedRuntime, SharedRuntimeError, WorkerEntry, WorkerHandle,
7};
8use crate::worker::Worker;
9use futures::stream::{FuturesUnordered, StreamExt};
10use libdd_common::MutexExt;
11use std::io;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14use tracing::{debug, error, warn};
15
16#[derive(Debug)]
28pub struct BasicRuntime {
29 runtime: Arc<tokio::runtime::Runtime>,
30 workers: Arc<Mutex<Vec<WorkerEntry>>>,
31 next_worker_id: AtomicU64,
32}
33
34impl BasicRuntime {
35 pub fn with_worker_threads(worker_threads: usize) -> Result<Self, SharedRuntimeError> {
38 let runtime = tokio::runtime::Builder::new_multi_thread()
39 .worker_threads(worker_threads)
40 .enable_all()
41 .build()?;
42 Ok(Self::from_handle(Arc::new(runtime)))
43 }
44
45 pub fn from_handle(runtime: Arc<tokio::runtime::Runtime>) -> Self {
50 Self {
51 runtime,
52 workers: Arc::new(Mutex::new(Vec::new())),
53 next_worker_id: AtomicU64::new(1),
54 }
55 }
56
57 fn push_worker(
58 &self,
59 workers_guard: &mut std::sync::MutexGuard<Vec<WorkerEntry>>,
60 pausable_worker: PausableWorker<BoxedWorker>,
61 ) -> WorkerHandle {
62 let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed);
63 workers_guard.push(WorkerEntry {
64 id: worker_id,
65 restart_on_fork: false,
66 worker: pausable_worker,
67 });
68 WorkerHandle {
69 worker_id,
70 workers: self.workers.clone(),
71 }
72 }
73}
74
75impl SharedRuntime for BasicRuntime {
76 fn new() -> Result<Self, SharedRuntimeError> {
77 Self::with_worker_threads(1)
78 }
79
80 fn spawn_worker<T: Worker + Sync + 'static>(
81 &self,
82 worker: T,
83 restart_on_fork: bool,
84 ) -> Result<WorkerHandle, SharedRuntimeError> {
85 if restart_on_fork {
86 warn!(
87 "restart_on_fork is ignored on BasicRuntime: regular mode is not fork-safe; \
88 use ForkSafeRuntime if you need fork hooks"
89 );
90 }
91
92 let boxed_worker: BoxedWorker = Box::new(worker);
93 debug!(?boxed_worker, "Spawning worker on BasicRuntime");
94 let mut pausable_worker = PausableWorker::new(boxed_worker);
95
96 let mut workers_guard = self.workers.lock_or_panic();
100 pausable_worker.start(tokio_spawn_fn(self.runtime.handle()))?;
101 Ok(self.push_worker(&mut workers_guard, pausable_worker))
102 }
103
104 async fn shutdown_async(&self) {
105 debug!("Shutting down all workers on BasicRuntime");
106 let workers = {
107 let mut workers_lock = self.workers.lock_or_panic();
108 std::mem::take(&mut *workers_lock)
109 };
110
111 let futures: FuturesUnordered<_> = workers
112 .into_iter()
113 .map(|mut worker_entry| async move {
114 if let Err(e) = worker_entry.worker.pause().await {
115 error!("Worker failed to shutdown: {:?}", e);
116 return;
117 }
118 worker_entry.worker.shutdown().await;
119 })
120 .collect();
121
122 futures.collect::<()>().await;
123 }
124}
125
126impl BlockingRuntime for BasicRuntime {
127 fn block_on<F: std::future::Future>(&self, f: F) -> Result<F::Output, io::Error> {
128 Ok(self.runtime.block_on(f))
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::shared_runtime::BlockOnTimeoutError;
136 use async_trait::async_trait;
137 use std::sync::mpsc::{channel, Receiver, Sender};
138 use std::time::Duration;
139 use tokio::time::sleep;
140
141 #[derive(Debug)]
142 struct TestWorker {
143 state: i32,
144 sender: Sender<i32>,
145 }
146
147 fn make_test_worker() -> (TestWorker, Receiver<i32>) {
148 let (sender, receiver) = channel::<i32>();
149 (TestWorker { state: 0, sender }, receiver)
150 }
151
152 #[async_trait]
153 impl Worker for TestWorker {
154 async fn run(&mut self) {
155 let _ = self.sender.send(self.state);
156 self.state += 1;
157 }
158
159 async fn trigger(&mut self) {
160 sleep(Duration::from_millis(100)).await;
161 }
162
163 async fn shutdown(&mut self) {
164 self.state = -1;
165 let _ = self.sender.send(self.state);
166 }
167 }
168
169 fn new_outer_runtime() -> Arc<tokio::runtime::Runtime> {
170 Arc::new(
171 tokio::runtime::Builder::new_multi_thread()
172 .worker_threads(1)
173 .enable_all()
174 .build()
175 .expect("failed to build outer runtime for BasicRuntime test"),
176 )
177 }
178
179 #[test]
180 fn test_new_lib_built_runtime_spawn_worker_runs() {
181 let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
182 let (worker, receiver) = make_test_worker();
183
184 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
185
186 assert_eq!(
187 receiver
188 .recv_timeout(Duration::from_secs(1))
189 .expect("worker did not run on library-built BasicRuntime"),
190 0
191 );
192 }
193
194 #[test]
195 fn test_with_worker_threads_spawn_worker_runs() {
196 let shared_runtime =
197 BasicRuntime::with_worker_threads(2).expect("BasicRuntime::with_worker_threads");
198 let (worker, receiver) = make_test_worker();
199
200 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
201
202 assert_eq!(
203 receiver
204 .recv_timeout(Duration::from_secs(1))
205 .expect("worker did not run on multi-thread BasicRuntime"),
206 0
207 );
208 }
209
210 #[test]
211 fn test_spawn_worker_ignores_restart_on_fork() {
212 let rt = new_outer_runtime();
213 let shared_runtime = BasicRuntime::from_handle(rt);
214 let (worker, receiver) = make_test_worker();
215
216 let _handle = shared_runtime
217 .spawn_worker(worker, true)
218 .expect("restart_on_fork=true should be silently ignored on BasicRuntime");
219
220 assert_eq!(
221 receiver
222 .recv_timeout(Duration::from_secs(1))
223 .expect("worker did not run when restart_on_fork was passed"),
224 0
225 );
226 }
227
228 #[test]
229 fn test_from_handle_spawn_worker_runs() {
230 let rt = new_outer_runtime();
231 let shared_runtime = BasicRuntime::from_handle(rt);
232 let (worker, receiver) = make_test_worker();
233
234 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
235
236 assert_eq!(
237 receiver
238 .recv_timeout(Duration::from_secs(1))
239 .expect("worker did not run on BasicRuntime"),
240 0
241 );
242 }
243
244 #[test]
245 fn test_shutdown_async_stops_workers_only() {
246 let rt = new_outer_runtime();
247 let shared_runtime = BasicRuntime::from_handle(rt.clone());
248 let (worker, receiver) = make_test_worker();
249
250 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
251 receiver
252 .recv_timeout(Duration::from_secs(1))
253 .expect("worker did not run before shutdown");
254
255 rt.block_on(shared_runtime.shutdown_async());
256
257 let mut last = receiver
259 .recv_timeout(Duration::from_secs(1))
260 .expect("shutdown did not send a value");
261 while let Ok(v) = receiver.try_recv() {
262 last = v;
263 }
264 assert_eq!(last, -1);
265 assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
266
267 rt.block_on(async { sleep(Duration::from_millis(10)).await });
269 }
270
271 #[test]
272 fn test_block_on_with_timeout_completes() {
273 let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
274
275 let value = shared_runtime
276 .block_on_with_timeout(async { 42 }, Duration::from_secs(1))
277 .expect("block_on_with_timeout should complete before the deadline");
278
279 assert_eq!(value, 42);
280 }
281
282 #[test]
283 fn test_block_on_with_timeout_times_out() {
284 let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
285
286 let err = shared_runtime
287 .block_on_with_timeout(
288 async { sleep(Duration::from_secs(60)).await },
289 Duration::from_millis(10),
290 )
291 .expect_err("block_on_with_timeout should time out before the sleep completes");
292
293 assert!(matches!(err, BlockOnTimeoutError::TimedOut(_)));
294 }
295
296 #[test]
297 fn test_block_on_with_timeout_errors_when_timers_disabled() {
298 let rt = Arc::new(
303 tokio::runtime::Builder::new_current_thread()
304 .build()
305 .expect("failed to build a timerless runtime for this test"),
306 );
307 let shared_runtime = BasicRuntime::from_handle(rt);
308
309 let err = shared_runtime
310 .block_on_with_timeout(async { 42 }, Duration::from_secs(1))
311 .expect_err("block_on_with_timeout should error when timers are disabled");
312
313 assert!(matches!(err, BlockOnTimeoutError::Io(_)));
314 }
315
316 #[test]
317 fn test_block_on_with_timeout_errors_when_timers_disabled_multi_thread() {
318 let rt = Arc::new(
319 tokio::runtime::Builder::new_multi_thread()
320 .worker_threads(1)
321 .build()
322 .expect("failed to build a timerless multi-thread runtime for this test"),
323 );
324 let shared_runtime = BasicRuntime::from_handle(rt);
325
326 let err = shared_runtime
327 .block_on_with_timeout(async { 42 }, Duration::from_secs(1))
328 .expect_err("block_on_with_timeout should error when timers are disabled");
329
330 assert!(matches!(err, BlockOnTimeoutError::Io(_)));
331 }
332
333 #[test]
334 fn test_keeps_runtime_alive_after_caller_drops() {
335 let rt = new_outer_runtime();
336 let shared_runtime = BasicRuntime::from_handle(rt.clone());
337 drop(rt);
340 let (worker, receiver) = make_test_worker();
341
342 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
343
344 assert_eq!(
345 receiver
346 .recv_timeout(Duration::from_secs(1))
347 .expect("worker did not run after caller dropped its runtime clone"),
348 0
349 );
350 }
351}