1use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct WorkerId(pub u32);
25
26#[derive(Debug, Clone, Copy)]
27pub struct WorkerHealth {
28 pub in_flight: u32,
30 pub completed: u64,
32 pub errored: u64,
34}
35
36pub trait Worker<Req, Resp>: Send + Sync {
40 fn id(&self) -> WorkerId;
41 fn health(&self) -> WorkerHealth;
42 fn dispatch(&self, req: Req) -> Result<Resp, WorkerError>;
45}
46
47#[derive(Debug, Clone)]
48pub enum WorkerError {
49 Domain { reason: String },
52 WorkerCrash { reason: String },
55}
56
57impl std::fmt::Display for WorkerError {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 Self::Domain { reason } => write!(f, "domain error: {reason}"),
61 Self::WorkerCrash { reason } => write!(f, "worker crash: {reason}"),
62 }
63 }
64}
65
66impl std::error::Error for WorkerError {}
67
68pub struct InProcessWorker<Req, Resp, F>
72where
73 F: Fn(Req) -> Result<Resp, WorkerError> + Send + Sync,
74{
75 id: WorkerId,
76 handler: F,
77 in_flight: AtomicU32,
78 completed: AtomicU64,
79 errored: AtomicU64,
80 _p: std::marker::PhantomData<fn() -> (Req, Resp)>,
83}
84
85impl<Req, Resp, F> InProcessWorker<Req, Resp, F>
86where
87 F: Fn(Req) -> Result<Resp, WorkerError> + Send + Sync,
88{
89 pub fn new(id: WorkerId, handler: F) -> Self {
90 Self {
91 id,
92 handler,
93 in_flight: AtomicU32::new(0),
94 completed: AtomicU64::new(0),
95 errored: AtomicU64::new(0),
96 _p: std::marker::PhantomData,
97 }
98 }
99}
100
101impl<Req, Resp, F> Worker<Req, Resp> for InProcessWorker<Req, Resp, F>
102where
103 Req: Send,
104 Resp: Send,
105 F: Fn(Req) -> Result<Resp, WorkerError> + Send + Sync,
106{
107 fn id(&self) -> WorkerId {
108 self.id
109 }
110
111 fn health(&self) -> WorkerHealth {
112 WorkerHealth {
113 in_flight: self.in_flight.load(Ordering::Relaxed),
114 completed: self.completed.load(Ordering::Relaxed),
115 errored: self.errored.load(Ordering::Relaxed),
116 }
117 }
118
119 fn dispatch(&self, req: Req) -> Result<Resp, WorkerError> {
120 self.in_flight.fetch_add(1, Ordering::Relaxed);
121 let result = (self.handler)(req);
122 self.in_flight.fetch_sub(1, Ordering::Relaxed);
123 self.completed.fetch_add(1, Ordering::Relaxed);
124 if result.is_err() {
125 self.errored.fetch_add(1, Ordering::Relaxed);
126 }
127 result
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum DispatchPolicy {
134 RoundRobin,
136 LeastLoaded,
139}
140
141pub struct WorkerPool<Req, Resp> {
144 workers: Vec<Arc<dyn Worker<Req, Resp>>>,
145 next_rr: Mutex<usize>,
146 pub policy: DispatchPolicy,
147}
148
149impl<Req, Resp> WorkerPool<Req, Resp> {
150 pub fn new(policy: DispatchPolicy) -> Self {
151 Self {
152 workers: Vec::new(),
153 next_rr: Mutex::new(0),
154 policy,
155 }
156 }
157
158 pub fn add(&mut self, worker: Arc<dyn Worker<Req, Resp>>) {
159 self.workers.push(worker);
160 }
161
162 pub fn len(&self) -> usize {
163 self.workers.len()
164 }
165 pub fn is_empty(&self) -> bool {
166 self.workers.is_empty()
167 }
168
169 pub fn select(&self) -> Option<&Arc<dyn Worker<Req, Resp>>> {
171 if self.workers.is_empty() {
172 return None;
173 }
174 match self.policy {
175 DispatchPolicy::RoundRobin => {
176 let mut rr = self.next_rr.lock().unwrap();
177 let pick = *rr % self.workers.len();
178 *rr = (*rr + 1) % self.workers.len();
179 Some(&self.workers[pick])
180 }
181 DispatchPolicy::LeastLoaded => self
182 .workers
183 .iter()
184 .min_by_key(|w| (w.health().in_flight, w.id().0)),
185 }
186 }
187
188 pub fn dispatch(&self, req: Req) -> Result<Resp, WorkerError> {
190 match self.select() {
191 Some(w) => w.dispatch(req),
192 None => Err(WorkerError::WorkerCrash {
193 reason: "no workers available".into(),
194 }),
195 }
196 }
197
198 pub fn health(&self) -> Vec<(WorkerId, WorkerHealth)> {
200 let mut h: Vec<_> = self.workers.iter().map(|w| (w.id(), w.health())).collect();
201 h.sort_by_key(|(id, _)| id.0);
202 h
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn make_worker(
211 id: u32,
212 ) -> Arc<InProcessWorker<u32, u32, impl Fn(u32) -> Result<u32, WorkerError> + Send + Sync>>
213 {
214 Arc::new(InProcessWorker::new(WorkerId(id), |x: u32| Ok(x * 2)))
215 }
216
217 #[test]
218 fn in_process_worker_handles_dispatch() {
219 let w = make_worker(7);
220 assert_eq!(w.dispatch(5).unwrap(), 10);
221 let h = w.health();
222 assert_eq!(h.completed, 1);
223 assert_eq!(h.errored, 0);
224 assert_eq!(h.in_flight, 0);
225 }
226
227 #[test]
228 fn errors_increment_errored_count() {
229 let w: Arc<InProcessWorker<u32, u32, _>> =
230 Arc::new(InProcessWorker::new(WorkerId(1), |_x: u32| {
231 Err(WorkerError::Domain {
232 reason: "bad".into(),
233 })
234 }));
235 let _ = w.dispatch(1);
236 let h = w.health();
237 assert_eq!(h.errored, 1);
238 assert_eq!(h.completed, 1);
239 }
240
241 #[test]
242 fn round_robin_visits_each_worker() {
243 let mut pool: WorkerPool<u32, u32> = WorkerPool::new(DispatchPolicy::RoundRobin);
244 for i in 0..3 {
245 pool.add(make_worker(i));
246 }
247
248 let mut ids = Vec::new();
249 for _ in 0..6 {
250 let w = pool.select().unwrap();
251 ids.push(w.id().0);
252 }
253 assert_eq!(ids, vec![0, 1, 2, 0, 1, 2]);
256 }
257
258 #[test]
259 fn least_loaded_picks_quietest() {
260 let w0 = make_worker(0);
263 let w1 = make_worker(1);
264 let w2 = make_worker(2);
265 w0.in_flight.fetch_add(5, Ordering::Relaxed);
267 w1.in_flight.fetch_add(3, Ordering::Relaxed);
268
269 let mut pool: WorkerPool<u32, u32> = WorkerPool::new(DispatchPolicy::LeastLoaded);
270 pool.add(w0);
271 pool.add(w1);
272 pool.add(w2);
273
274 let pick = pool.select().unwrap();
275 assert_eq!(
276 pick.id().0,
277 2,
278 "least-loaded should pick the worker with 0 in-flight"
279 );
280 }
281
282 #[test]
283 fn empty_pool_dispatch_errors() {
284 let pool: WorkerPool<u32, u32> = WorkerPool::new(DispatchPolicy::RoundRobin);
285 let err = pool.dispatch(1).unwrap_err();
286 assert!(matches!(err, WorkerError::WorkerCrash { .. }));
287 }
288
289 #[test]
290 fn health_snapshot_includes_every_worker() {
291 let mut pool: WorkerPool<u32, u32> = WorkerPool::new(DispatchPolicy::RoundRobin);
292 for i in 0..3 {
293 pool.add(make_worker(i));
294 }
295 let _ = pool.dispatch(1);
296 let _ = pool.dispatch(2);
297 let h = pool.health();
298 assert_eq!(h.len(), 3);
299 let total_completed: u64 = h.iter().map(|(_, hh)| hh.completed).sum();
300 assert_eq!(total_completed, 2);
301 }
302}