orinium_browser/engine/
background_worker.rs1use std::sync::mpsc::{self, Receiver, Sender};
2use std::sync::{Arc, Mutex};
3use std::thread;
4
5pub struct BackgroundWorker<C, R> {
17 cmd_tx: Sender<C>,
18 result_rx: Receiver<R>,
19}
20
21impl<C, R> std::fmt::Debug for BackgroundWorker<C, R> {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.debug_struct("BackgroundWorker").finish()
24 }
25}
26
27impl<C: Send + 'static, R: Send + 'static> BackgroundWorker<C, R> {
28 pub fn new<F>(worker_count: usize, process: F) -> Self
31 where
32 F: Fn(C) -> R + Send + Sync + 'static,
33 {
34 Self::new_with_init(worker_count, || (), move |(), cmd| process(cmd))
35 }
36
37 pub fn new_with_init<T, F, G>(worker_count: usize, init: F, process: G) -> Self
44 where
45 T: 'static,
46 F: Fn() -> T + Send + Sync + 'static,
47 G: Fn(&T, C) -> R + Send + Sync + 'static,
48 {
49 let (cmd_tx, cmd_rx) = mpsc::channel::<C>();
50 let (result_tx, result_rx) = mpsc::channel::<R>();
51 let cmd_rx = Arc::new(Mutex::new(cmd_rx));
52
53 let init = Arc::new(init);
54 let process = Arc::new(process);
55 for _ in 0..worker_count {
56 let cmd_rx = Arc::clone(&cmd_rx);
57 let result_tx = result_tx.clone();
58 let init = Arc::clone(&init);
59 let process = Arc::clone(&process);
60 thread::spawn(move || {
61 let ctx = init();
62 loop {
63 let cmd = match cmd_rx.lock().unwrap().recv() {
64 Ok(cmd) => cmd,
65 Err(_) => {
66 log::debug!("BackgroundWorker: worker exiting, command channel closed");
67 break;
68 }
69 };
70 let result = process(&ctx, cmd);
71 let _ = result_tx.send(result);
72 }
73 });
74 }
75
76 Self { cmd_tx, result_rx }
77 }
78
79 pub fn send(&self, cmd: C) {
86 if self.cmd_tx.send(cmd).is_err() {
87 log::error!("BackgroundWorker: command dropped, all worker threads have exited");
88 }
89 }
90
91 pub fn try_receive(&self) -> Option<R> {
94 self.result_rx.try_recv().ok()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::time::Duration;
101
102 use super::*;
103
104 fn wait_for_results<C, R>(worker: &BackgroundWorker<C, R>, count: usize) -> Vec<R>
105 where
106 C: Send + 'static,
107 R: Send + 'static,
108 {
109 let deadline = std::time::Instant::now() + Duration::from_secs(5);
110 let mut results = Vec::new();
111 while results.len() < count {
112 if let Some(result) = worker.try_receive() {
113 results.push(result);
114 continue;
115 }
116 assert!(
117 std::time::Instant::now() < deadline,
118 "worker results did not arrive before the timeout"
119 );
120 thread::sleep(Duration::from_millis(1));
121 }
122 results
123 }
124
125 #[test]
126 fn every_command_is_processed_exactly_once() {
127 const COMMANDS: usize = 32;
128 let worker = BackgroundWorker::new(4, |n: usize| n * 2);
129 for n in 0..COMMANDS {
130 worker.send(n);
131 }
132
133 let mut results = wait_for_results(&worker, COMMANDS);
134 results.sort_unstable();
135 let doubled: Vec<_> = (0..COMMANDS).map(|n| n * 2).collect();
136 assert_eq!(results, doubled);
137 }
138
139 #[test]
140 fn init_runs_once_per_worker_and_context_is_shared_by_that_thread() {
141 use std::sync::atomic::{AtomicUsize, Ordering};
142
143 static INIT_CALLS: AtomicUsize = AtomicUsize::new(0);
144 const WORKERS: usize = 3;
145 const COMMANDS: usize = 12;
146
147 let worker = BackgroundWorker::<(), usize>::new_with_init(
148 WORKERS,
149 || INIT_CALLS.fetch_add(1, Ordering::SeqCst),
150 |ctx, (): ()| *ctx,
151 );
152 for _ in 0..COMMANDS {
153 worker.send(());
154 }
155
156 let results = wait_for_results(&worker, COMMANDS);
157 assert!(results.iter().all(|ctx| *ctx < WORKERS));
159 assert_eq!(
160 INIT_CALLS.load(Ordering::SeqCst),
161 WORKERS,
162 "each worker must initialize its context exactly once"
163 );
164 }
165
166 #[test]
167 fn non_send_context_is_accepted() {
168 #[allow(dead_code)]
169 struct NotSend(std::rc::Rc<()>);
170 let worker = BackgroundWorker::<usize, usize>::new_with_init(
171 2,
172 || NotSend(std::rc::Rc::new(())),
173 |_ctx, n: usize| n + 1,
174 );
175 for n in 0..4 {
176 worker.send(n);
177 }
178 let mut results = wait_for_results(&worker, 4);
179 results.sort_unstable();
180 assert_eq!(results, vec![1, 2, 3, 4]);
181 }
182}