Skip to main content

ractor/factory/
routing.rs

1// Copyright (c) Sean Lawlor
2//
3// This source code is licensed under both the MIT license found in the
4// LICENSE-MIT file in the root directory of this source tree.
5
6//! Routing protocols for Factories
7
8use std::collections::HashMap;
9use std::collections::VecDeque;
10use std::marker::PhantomData;
11
12use crate::factory::worker::WorkerProperties;
13use crate::factory::Job;
14use crate::factory::JobKey;
15use crate::factory::WorkerId;
16use crate::ActorProcessingErr;
17use crate::Message;
18use crate::State;
19
20/// Custom hashing behavior for factory routing to workers
21pub trait CustomHashFunction<TKey>: Send + Sync
22where
23    TKey: Send + Sync + 'static,
24{
25    /// Hash the key into the space 0..usize
26    fn hash(&self, key: &TKey, worker_count: usize) -> usize;
27}
28
29/// The possible results from a routing operation.
30#[derive(Debug)]
31pub enum RouteResult<TKey, TMsg>
32where
33    TKey: JobKey,
34    TMsg: Message,
35{
36    /// The job has been handled and routed successfully
37    Handled,
38    /// The job needs to be backlogged into the internal factory's queue (if
39    /// configured)
40    Backlog(Job<TKey, TMsg>),
41    /// The job has exceeded the internal rate limit specification of the router.
42    /// This would be returned as a route operation in the event that the router is
43    /// tracking the jobs-per-unit-time and has decided that routing this next job
44    /// would exceed that limit.
45    ///
46    /// Returns the job that was rejected
47    RateLimited(Job<TKey, TMsg>),
48}
49
50/// A routing mode controls how a request is routed from the factory to a
51/// designated worker
52pub trait Router<TKey, TMsg>: State
53where
54    TKey: JobKey,
55    TMsg: Message,
56{
57    /// Route a [Job] based on the specific routing methodology
58    ///
59    /// * `job` - The job to be routed
60    /// * `pool_size` - The size of the ACTIVE worker pool (excluding draining workers)
61    /// * `worker_hint` - If provided, this is a "hint" at which worker should receive the job,
62    ///   if available.
63    /// * `worker_pool` - The current worker pool, which may contain draining workers
64    ///
65    /// Returns [RouteResult::Handled] if the job was routed successfully, otherwise
66    /// [RouteResult::Backlog] is returned indicating that the job should be enqueued in
67    /// the factory's internal queue.
68    fn route_message(
69        &mut self,
70        job: Job<TKey, TMsg>,
71        pool_size: usize,
72        worker_hint: Option<WorkerId>,
73        worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
74    ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr>;
75
76    /// Identifies if a job CAN be routed, and to which worker, without
77    /// requiring dequeueing the job
78    ///
79    /// This prevents the need to support pushing jobs that have been dequeued,
80    /// but no worker is available to accept the job, back into the front of the
81    /// queue. And given the single-threaded nature of a Factory, this is safe
82    /// to call outside of a locked context. It is assumed that if this returns
83    /// [Some(WorkerId)], then the job is guaranteed to be routed, as internal state to
84    /// the router may be updated.
85    ///
86    ///  * `job` - A reference to the job to be routed
87    /// * `pool_size` - The size of the ACTIVE worker pool (excluding draining workers)
88    /// * `worker_hint` - If provided, this is a "hint" at which worker should receive the job,
89    ///   if available.
90    /// * `worker_pool` - The current worker pool, which may contain draining workers
91    ///
92    /// Returns [None] if no worker can be identified or no worker is avaialble to accept
93    /// the job, otherwise [Some(WorkerId)] indicating the target worker is returned
94    fn choose_target_worker(
95        &mut self,
96        job: &Job<TKey, TMsg>,
97        pool_size: usize,
98        worker_hint: Option<WorkerId>,
99        worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
100    ) -> Option<WorkerId>;
101
102    /// Returns a flag indicating if the factory does discard/overload management ([true])
103    /// or if is handled by the workers worker(s) ([false])
104    fn is_factory_queueing(&self) -> bool;
105
106    /// Notification that a worker's availability has changed.
107    ///
108    /// Called by the factory when a worker transitions between available and busy states.
109    /// Routers can use this to maintain an index of available workers for O(1) dispatch.
110    ///
111    /// * `wid` - The worker id whose availability changed
112    /// * `available` - `true` if the worker is now available, `false` if now busy
113    fn on_worker_availability_change(&mut self, _wid: WorkerId, _available: bool) {}
114}
115
116// ============================ Macros ======================= //
117macro_rules! impl_routing_mode {
118    ($routing_mode: ident, $doc:expr) => {
119        #[doc = $doc]
120        #[derive(Debug)]
121        pub struct $routing_mode<TKey, TMsg>
122        where
123            TKey: JobKey,
124            TMsg: Message,
125        {
126            _key: PhantomData<fn() -> TKey>,
127            _msg: PhantomData<fn() -> TMsg>,
128        }
129
130        impl<TKey, TMsg> Default for $routing_mode<TKey, TMsg>
131        where
132            TKey: JobKey,
133            TMsg: Message,
134        {
135            fn default() -> Self {
136                Self {
137                    _key: PhantomData,
138                    _msg: PhantomData,
139                }
140            }
141        }
142    };
143}
144
145// ============================ Key Persistent routing ======================= //
146impl_routing_mode! {KeyPersistentRouting, "Factory will select worker by hashing the job's key.
147Workers will have jobs placed into their incoming message queue's"}
148
149impl<TKey, TMsg> Router<TKey, TMsg> for KeyPersistentRouting<TKey, TMsg>
150where
151    TKey: JobKey,
152    TMsg: Message,
153{
154    fn route_message(
155        &mut self,
156        job: Job<TKey, TMsg>,
157        pool_size: usize,
158        worker_hint: Option<WorkerId>,
159        worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
160    ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr> {
161        if let Some(worker) = self
162            .choose_target_worker(&job, pool_size, worker_hint, worker_pool)
163            .and_then(|wid| worker_pool.get_mut(&wid))
164        {
165            worker.enqueue_job(job)?;
166        }
167        Ok(RouteResult::Handled)
168    }
169
170    fn choose_target_worker(
171        &mut self,
172        job: &Job<TKey, TMsg>,
173        pool_size: usize,
174        worker_hint: Option<WorkerId>,
175        _worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
176    ) -> Option<WorkerId> {
177        let key =
178            worker_hint.unwrap_or_else(|| crate::factory::hash::hash_with_max(&job.key, pool_size));
179        Some(key)
180    }
181
182    fn is_factory_queueing(&self) -> bool {
183        false
184    }
185}
186
187// ============================ Queuer routing ======================= //
188/// Factory will dispatch job to first available worker.
189/// Factory will maintain shared internal queue of messages
190#[derive(Debug)]
191pub struct QueuerRouting<TKey, TMsg>
192where
193    TKey: JobKey,
194    TMsg: Message,
195{
196    _key: PhantomData<fn() -> TKey>,
197    _msg: PhantomData<fn() -> TMsg>,
198    /// FIFO deque of workers believed to be available
199    available_workers: VecDeque<WorkerId>,
200    /// Indexed by WorkerId — true if the worker is already in `available_workers`
201    worker_in_queue: Vec<bool>,
202}
203
204impl<TKey, TMsg> Default for QueuerRouting<TKey, TMsg>
205where
206    TKey: JobKey,
207    TMsg: Message,
208{
209    fn default() -> Self {
210        Self {
211            _key: PhantomData,
212            _msg: PhantomData,
213            available_workers: VecDeque::new(),
214            worker_in_queue: Vec::new(),
215        }
216    }
217}
218
219impl<TKey, TMsg> Router<TKey, TMsg> for QueuerRouting<TKey, TMsg>
220where
221    TKey: JobKey,
222    TMsg: Message,
223{
224    fn route_message(
225        &mut self,
226        job: Job<TKey, TMsg>,
227        pool_size: usize,
228        worker_hint: Option<WorkerId>,
229        worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
230    ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr> {
231        if let Some(worker) = self
232            .choose_target_worker(&job, pool_size, worker_hint, worker_pool)
233            .and_then(|wid| worker_pool.get_mut(&wid))
234        {
235            worker.enqueue_job(job)?;
236            Ok(RouteResult::Handled)
237        } else {
238            Ok(RouteResult::Backlog(job))
239        }
240    }
241
242    fn choose_target_worker(
243        &mut self,
244        _job: &Job<TKey, TMsg>,
245        _pool_size: usize,
246        worker_hint: Option<WorkerId>,
247        worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
248    ) -> Option<WorkerId> {
249        if let Some(worker) = worker_hint.and_then(|worker| worker_pool.get(&worker)) {
250            if worker.is_available() {
251                return worker_hint;
252            }
253        }
254        // Pop from the available-workers deque, skipping stale entries
255        while let Some(wid) = self.available_workers.pop_front() {
256            if wid < self.worker_in_queue.len() {
257                self.worker_in_queue[wid] = false;
258            }
259            if let Some(worker) = worker_pool.get(&wid) {
260                if worker.is_available() {
261                    return Some(wid);
262                }
263            }
264            // Worker removed from pool or no longer available — skip
265        }
266        None
267    }
268
269    fn is_factory_queueing(&self) -> bool {
270        true
271    }
272
273    fn on_worker_availability_change(&mut self, wid: WorkerId, available: bool) {
274        // Grow the tracking vec if needed
275        if wid >= self.worker_in_queue.len() {
276            self.worker_in_queue.resize(wid + 1, false);
277        }
278        if available {
279            if !self.worker_in_queue[wid] {
280                self.worker_in_queue[wid] = true;
281                self.available_workers.push_back(wid);
282            }
283        } else {
284            // Mark not-in-queue; lazy removal from deque
285            self.worker_in_queue[wid] = false;
286        }
287    }
288}
289
290// ============================ Sticky Queuer routing ======================= //
291/// Factory will dispatch jobs to a worker that is processing the same key (if any).
292/// Factory will maintain shared internal queue of messages.
293///
294/// Note: This is helpful for sharded db access style scenarios. If a worker is
295/// currently doing something on a given row id for example, we want subsequent updates
296/// to land on the same worker so it can serialize updates to the same row consistently.
297#[derive(Debug)]
298pub struct StickyQueuerRouting<TKey, TMsg>
299where
300    TKey: JobKey,
301    TMsg: Message,
302{
303    _key: PhantomData<fn() -> TKey>,
304    _msg: PhantomData<fn() -> TMsg>,
305    /// FIFO deque of workers believed to be available
306    available_workers: VecDeque<WorkerId>,
307    /// Indexed by WorkerId — true if the worker is already in `available_workers`
308    worker_in_queue: Vec<bool>,
309}
310
311impl<TKey, TMsg> Default for StickyQueuerRouting<TKey, TMsg>
312where
313    TKey: JobKey,
314    TMsg: Message,
315{
316    fn default() -> Self {
317        Self {
318            _key: PhantomData,
319            _msg: PhantomData,
320            available_workers: VecDeque::new(),
321            worker_in_queue: Vec::new(),
322        }
323    }
324}
325
326impl<TKey, TMsg> Router<TKey, TMsg> for StickyQueuerRouting<TKey, TMsg>
327where
328    TKey: JobKey,
329    TMsg: Message,
330{
331    fn route_message(
332        &mut self,
333        job: Job<TKey, TMsg>,
334        pool_size: usize,
335        worker_hint: Option<WorkerId>,
336        worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
337    ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr> {
338        if let Some(worker) = self
339            .choose_target_worker(&job, pool_size, worker_hint, worker_pool)
340            .and_then(|wid| worker_pool.get_mut(&wid))
341        {
342            worker.enqueue_job(job)?;
343            Ok(RouteResult::Handled)
344        } else {
345            Ok(RouteResult::Backlog(job))
346        }
347    }
348
349    fn choose_target_worker(
350        &mut self,
351        job: &Job<TKey, TMsg>,
352        _pool_size: usize,
353        worker_hint: Option<WorkerId>,
354        worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
355    ) -> Option<WorkerId> {
356        // check sticky first
357        if let Some(worker) = worker_hint.and_then(|worker| worker_pool.get(&worker)) {
358            if worker.is_processing_key(&job.key) {
359                return worker_hint;
360            }
361        }
362
363        let maybe_worker = worker_pool
364            .iter()
365            .find(|(_, worker)| worker.is_processing_key(&job.key))
366            .map(|(a, _)| *a);
367        if maybe_worker.is_some() {
368            return maybe_worker;
369        }
370
371        // now take first available, based on hint then deque
372        if let Some(worker) = worker_hint.and_then(|worker| worker_pool.get(&worker)) {
373            if worker.is_available() {
374                return worker_hint;
375            }
376        }
377
378        // fallback to first free worker via the available-workers deque
379        while let Some(wid) = self.available_workers.pop_front() {
380            if wid < self.worker_in_queue.len() {
381                self.worker_in_queue[wid] = false;
382            }
383            if let Some(worker) = worker_pool.get(&wid) {
384                if worker.is_available() {
385                    return Some(wid);
386                }
387            }
388            // Worker removed from pool or no longer available — skip
389        }
390        None
391    }
392
393    fn is_factory_queueing(&self) -> bool {
394        true
395    }
396
397    fn on_worker_availability_change(&mut self, wid: WorkerId, available: bool) {
398        // Grow the tracking vec if needed
399        if wid >= self.worker_in_queue.len() {
400            self.worker_in_queue.resize(wid + 1, false);
401        }
402        if available {
403            if !self.worker_in_queue[wid] {
404                self.worker_in_queue[wid] = true;
405                self.available_workers.push_back(wid);
406            }
407        } else {
408            // Mark not-in-queue; lazy removal from deque
409            self.worker_in_queue[wid] = false;
410        }
411    }
412}
413
414// ============================ Round-robin routing ======================= //
415/// Factory will dispatch to the next worker in order.
416///
417/// Workers will have jobs placed into their incoming message queue's
418#[derive(Debug)]
419pub struct RoundRobinRouting<TKey, TMsg>
420where
421    TKey: JobKey,
422    TMsg: Message,
423{
424    _key: PhantomData<fn() -> TKey>,
425    _msg: PhantomData<fn() -> TMsg>,
426    last_worker: WorkerId,
427}
428
429impl<TKey, TMsg> Default for RoundRobinRouting<TKey, TMsg>
430where
431    TKey: JobKey,
432    TMsg: Message,
433{
434    fn default() -> Self {
435        Self {
436            _key: PhantomData,
437            _msg: PhantomData,
438            last_worker: 0,
439        }
440    }
441}
442
443impl<TKey, TMsg> Router<TKey, TMsg> for RoundRobinRouting<TKey, TMsg>
444where
445    TKey: JobKey,
446    TMsg: Message,
447{
448    fn route_message(
449        &mut self,
450        job: Job<TKey, TMsg>,
451        pool_size: usize,
452        worker_hint: Option<WorkerId>,
453        worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
454    ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr> {
455        if let Some(worker) = self
456            .choose_target_worker(&job, pool_size, worker_hint, worker_pool)
457            .and_then(|wid| worker_pool.get_mut(&wid))
458        {
459            worker.enqueue_job(job)?;
460        }
461        Ok(RouteResult::Handled)
462    }
463
464    fn choose_target_worker(
465        &mut self,
466        _job: &Job<TKey, TMsg>,
467        pool_size: usize,
468        worker_hint: Option<WorkerId>,
469        worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
470    ) -> Option<WorkerId> {
471        if let Some(worker) = worker_hint.and_then(|worker| worker_pool.get(&worker)) {
472            if worker.is_available() {
473                return worker_hint;
474            }
475        }
476
477        let mut key = self.last_worker + 1;
478        if key >= pool_size {
479            key = 0;
480        }
481        self.last_worker = key;
482        Some(key)
483    }
484
485    fn is_factory_queueing(&self) -> bool {
486        false
487    }
488}
489
490// ============================ Custom routing ======================= //
491/// Factory will dispatch to workers based on a custom hash function.
492///
493/// The factory maintains no queue in this scenario, and jobs are pushed
494/// to worker's queues.
495#[derive(Debug)]
496pub struct CustomRouting<TKey, TMsg, THasher>
497where
498    TKey: JobKey,
499    TMsg: Message,
500    THasher: CustomHashFunction<TKey>,
501{
502    _key: PhantomData<fn() -> TKey>,
503    _msg: PhantomData<fn() -> TMsg>,
504    hasher: THasher,
505}
506
507impl<TKey, TMsg, THasher> CustomRouting<TKey, TMsg, THasher>
508where
509    TKey: JobKey,
510    TMsg: Message,
511    THasher: CustomHashFunction<TKey>,
512{
513    /// Construct a new [CustomRouting] instance with the supplied hash function
514    pub fn new(hasher: THasher) -> Self {
515        Self {
516            _key: PhantomData,
517            _msg: PhantomData,
518            hasher,
519        }
520    }
521}
522
523impl<TKey, TMsg, THasher> Router<TKey, TMsg> for CustomRouting<TKey, TMsg, THasher>
524where
525    TKey: JobKey,
526    TMsg: Message,
527    THasher: CustomHashFunction<TKey> + 'static,
528{
529    fn route_message(
530        &mut self,
531        job: Job<TKey, TMsg>,
532        pool_size: usize,
533        worker_hint: Option<WorkerId>,
534        worker_pool: &mut HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
535    ) -> Result<RouteResult<TKey, TMsg>, ActorProcessingErr> {
536        if let Some(worker) = self
537            .choose_target_worker(&job, pool_size, worker_hint, worker_pool)
538            .and_then(|wid| worker_pool.get_mut(&wid))
539        {
540            worker.enqueue_job(job)?;
541        }
542        Ok(RouteResult::Handled)
543    }
544
545    fn choose_target_worker(
546        &mut self,
547        job: &Job<TKey, TMsg>,
548        pool_size: usize,
549        _worker_hint: Option<WorkerId>,
550        _worker_pool: &HashMap<WorkerId, WorkerProperties<TKey, TMsg>>,
551    ) -> Option<WorkerId> {
552        let key = self.hasher.hash(&job.key, pool_size);
553        Some(key)
554    }
555
556    fn is_factory_queueing(&self) -> bool {
557        false
558    }
559}