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 async_trait::async_trait;
136 use std::sync::mpsc::{channel, Receiver, Sender};
137 use std::time::Duration;
138 use tokio::time::sleep;
139
140 #[derive(Debug)]
141 struct TestWorker {
142 state: i32,
143 sender: Sender<i32>,
144 }
145
146 fn make_test_worker() -> (TestWorker, Receiver<i32>) {
147 let (sender, receiver) = channel::<i32>();
148 (TestWorker { state: 0, sender }, receiver)
149 }
150
151 #[async_trait]
152 impl Worker for TestWorker {
153 async fn run(&mut self) {
154 let _ = self.sender.send(self.state);
155 self.state += 1;
156 }
157
158 async fn trigger(&mut self) {
159 sleep(Duration::from_millis(100)).await;
160 }
161
162 async fn shutdown(&mut self) {
163 self.state = -1;
164 let _ = self.sender.send(self.state);
165 }
166 }
167
168 fn new_outer_runtime() -> Arc<tokio::runtime::Runtime> {
169 Arc::new(
170 tokio::runtime::Builder::new_multi_thread()
171 .worker_threads(1)
172 .enable_all()
173 .build()
174 .expect("failed to build outer runtime for BasicRuntime test"),
175 )
176 }
177
178 #[test]
179 fn test_new_lib_built_runtime_spawn_worker_runs() {
180 let shared_runtime = BasicRuntime::new().expect("BasicRuntime::new");
181 let (worker, receiver) = make_test_worker();
182
183 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
184
185 assert_eq!(
186 receiver
187 .recv_timeout(Duration::from_secs(1))
188 .expect("worker did not run on library-built BasicRuntime"),
189 0
190 );
191 }
192
193 #[test]
194 fn test_with_worker_threads_spawn_worker_runs() {
195 let shared_runtime =
196 BasicRuntime::with_worker_threads(2).expect("BasicRuntime::with_worker_threads");
197 let (worker, receiver) = make_test_worker();
198
199 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
200
201 assert_eq!(
202 receiver
203 .recv_timeout(Duration::from_secs(1))
204 .expect("worker did not run on multi-thread BasicRuntime"),
205 0
206 );
207 }
208
209 #[test]
210 fn test_spawn_worker_ignores_restart_on_fork() {
211 let rt = new_outer_runtime();
212 let shared_runtime = BasicRuntime::from_handle(rt);
213 let (worker, receiver) = make_test_worker();
214
215 let _handle = shared_runtime
216 .spawn_worker(worker, true)
217 .expect("restart_on_fork=true should be silently ignored on BasicRuntime");
218
219 assert_eq!(
220 receiver
221 .recv_timeout(Duration::from_secs(1))
222 .expect("worker did not run when restart_on_fork was passed"),
223 0
224 );
225 }
226
227 #[test]
228 fn test_from_handle_spawn_worker_runs() {
229 let rt = new_outer_runtime();
230 let shared_runtime = BasicRuntime::from_handle(rt);
231 let (worker, receiver) = make_test_worker();
232
233 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
234
235 assert_eq!(
236 receiver
237 .recv_timeout(Duration::from_secs(1))
238 .expect("worker did not run on BasicRuntime"),
239 0
240 );
241 }
242
243 #[test]
244 fn test_shutdown_async_stops_workers_only() {
245 let rt = new_outer_runtime();
246 let shared_runtime = BasicRuntime::from_handle(rt.clone());
247 let (worker, receiver) = make_test_worker();
248
249 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
250 receiver
251 .recv_timeout(Duration::from_secs(1))
252 .expect("worker did not run before shutdown");
253
254 rt.block_on(shared_runtime.shutdown_async());
255
256 let mut last = receiver
258 .recv_timeout(Duration::from_secs(1))
259 .expect("shutdown did not send a value");
260 while let Ok(v) = receiver.try_recv() {
261 last = v;
262 }
263 assert_eq!(last, -1);
264 assert_eq!(shared_runtime.workers.lock_or_panic().len(), 0);
265
266 rt.block_on(async { sleep(Duration::from_millis(10)).await });
268 }
269
270 #[test]
271 fn test_keeps_runtime_alive_after_caller_drops() {
272 let rt = new_outer_runtime();
273 let shared_runtime = BasicRuntime::from_handle(rt.clone());
274 drop(rt);
277 let (worker, receiver) = make_test_worker();
278
279 let _handle = shared_runtime.spawn_worker(worker, false).unwrap();
280
281 assert_eq!(
282 receiver
283 .recv_timeout(Duration::from_secs(1))
284 .expect("worker did not run after caller dropped its runtime clone"),
285 0
286 );
287 }
288}