Skip to main content

fynd_core/worker_pool/
pool.rs

1//! Worker pool for processing solve tasks.
2//!
3//! The worker pool manages multiple dedicated OS threads for CPU-bound route finding.
4//! Each pool owns multiple SolverWorker instances that compete for tasks from the queue.
5//! A pool is configured with a specific algorithm (by name), allowing multiple pools
6//! with different algorithms to compete via the WorkerPoolRouter.
7//!
8//! Pools can use either a built-in algorithm (by name via [`WorkerPoolBuilder::algorithm`])
9//! or a custom [`Algorithm`](crate::algorithm::Algorithm) implementation (via
10//! [`WorkerPoolBuilder::with_algorithm`]).
11use std::thread::JoinHandle;
12
13use tokio::sync::broadcast;
14use tracing::{error, info};
15
16use crate::{
17    algorithm::AlgorithmConfig,
18    derived::{events::DerivedDataEvent, SharedDerivedDataRef},
19    feed::{
20        events::{MarketEvent, MarketEventHandler},
21        exclusivity::ExclusivityPolicy,
22        market_data::MarketData,
23    },
24    graph::EdgeWeightUpdaterWithDerived,
25    types::internal::SolveTask,
26    worker_pool::{
27        registry::{
28            spawn_workers_generic, AlgorithmSpawner, SpawnWorkersParams, UnknownAlgorithmError,
29            DEFAULT_ALGORITHM,
30        },
31        task_queue::{TaskQueue, TaskQueueConfig, TaskQueueHandle},
32    },
33};
34
35/// Configuration for the worker pool.
36#[derive(Debug)]
37pub struct WorkerPoolConfig {
38    /// Human-readable name for this pool (used in logging/metrics).
39    /// Can differ from algorithm to distinguish pools with same algorithm but different configs.
40    name: String,
41    /// How to spawn workers — either a built-in registry lookup or a custom factory.
42    spawner: AlgorithmSpawner,
43    /// Number of worker threads.
44    num_workers: usize,
45    /// Configuration for the algorithm used by each worker.
46    algorithm_config: AlgorithmConfig,
47    /// Task queue capacity (maximum number of pending tasks).
48    task_queue_capacity: usize,
49    /// When set, exclusive components are filtered out of this pool's workers' graphs
50    /// (default: `None`, no filtering).
51    ///
52    /// `All` is safe as the default because it only applies when no `ExclusivityPolicy` is
53    /// configured — meaning no exclusive components exist to exclude. When a policy is set,
54    /// `FyndBuilder::assemble_components` always constructs
55    /// `Some(policy)` for `Public`-scoped pools, so this default is never relied on in
56    /// that path.
57    exclusivity_policy: Option<ExclusivityPolicy>,
58}
59
60impl WorkerPoolConfig {
61    /// Returns the algorithm name for this pool.
62    pub fn algorithm_name(&self) -> &str {
63        self.spawner.algorithm_name()
64    }
65}
66
67impl Default for WorkerPoolConfig {
68    fn default() -> Self {
69        Self {
70            name: DEFAULT_ALGORITHM.to_string(),
71            spawner: AlgorithmSpawner::Registry { algorithm: DEFAULT_ALGORITHM.to_string() },
72            num_workers: num_cpus::get(),
73            algorithm_config: AlgorithmConfig::default(),
74            task_queue_capacity: 1000,
75            exclusivity_policy: None,
76        }
77    }
78}
79
80/// A pool of worker threads for processing solve tasks.
81///
82/// Each pool is dedicated to a specific algorithm. Workers in the pool
83/// compete for tasks from the shared queue.
84pub struct WorkerPool {
85    /// Human-readable name for this pool.
86    name: String,
87    /// Algorithm name for this pool.
88    algorithm: String,
89    /// Handles to worker threads.
90    workers: Vec<JoinHandle<()>>,
91    /// Shutdown signal sender.
92    shutdown_tx: broadcast::Sender<()>,
93}
94
95impl WorkerPool {
96    /// Spawns a new worker pool.
97    ///
98    /// # Arguments
99    ///
100    /// * `config` - Worker pool configuration
101    /// * `task_rx` - Receiver for tasks from the queue
102    /// * `market_data` - Shared market data reference
103    /// * `derived_data` - Shared derived data reference (pool depths, token prices)
104    /// * `event_rx` - Broadcast receiver for market events (workers subscribe to this)
105    /// * `derived_event_rx` - Broadcast receiver for derived data events (resubscribed per worker)
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the algorithm name in config is not registered.
110    pub fn spawn(
111        config: WorkerPoolConfig,
112        task_rx: async_channel::Receiver<SolveTask>,
113        market_data: MarketData,
114        derived_data: SharedDerivedDataRef,
115        event_rx: broadcast::Receiver<MarketEvent>,
116        derived_event_rx: broadcast::Receiver<DerivedDataEvent>,
117    ) -> Result<Self, UnknownAlgorithmError> {
118        let (shutdown_tx, _) = broadcast::channel(1);
119        let name = config.name.clone();
120        let algorithm = config
121            .spawner
122            .algorithm_name()
123            .to_string();
124
125        // Spawn workers
126        let exclusivity_policy = config.exclusivity_policy.clone();
127        let params = SpawnWorkersParams {
128            algorithm: algorithm.clone(),
129            pool_name: name.clone(),
130            num_workers: config.num_workers,
131            algorithm_config: config.algorithm_config,
132            task_rx,
133            market_data,
134            derived_data,
135            event_rx,
136            derived_event_rx,
137            shutdown_tx: shutdown_tx.clone(),
138            exclusivity_policy,
139        };
140        let workers = config.spawner.spawn(params)?;
141
142        info!(
143            name = %name,
144            algorithm = %algorithm,
145            num_workers = workers.len(),
146            "worker pool spawned"
147        );
148
149        Ok(Self { name, algorithm, workers, shutdown_tx })
150    }
151
152    /// Returns the pool name.
153    pub fn name(&self) -> &str {
154        &self.name
155    }
156
157    /// Returns the algorithm name for this pool.
158    pub fn algorithm(&self) -> &str {
159        &self.algorithm
160    }
161
162    /// Returns the number of workers.
163    pub fn num_workers(&self) -> usize {
164        self.workers.len()
165    }
166
167    /// Shuts down all workers and waits for them to finish.
168    pub fn shutdown(self) {
169        info!(name = %self.name, "shutting down worker pool");
170
171        // Send shutdown signal
172        let _ = self.shutdown_tx.send(());
173
174        // Wait for all workers to finish
175        for (i, handle) in self.workers.into_iter().enumerate() {
176            if let Err(e) = handle.join() {
177                error!(
178                    name = %self.name,
179                    worker_id = i,
180                    "worker thread panicked: {:?}",
181                    e
182                );
183            }
184        }
185
186        info!(name = %self.name, "worker pool shut down");
187    }
188}
189
190/// Builder for WorkerPool with a fluent API.
191///
192/// # Built-in algorithms
193///
194/// Use [`algorithm`](Self::algorithm) to select a built-in algorithm by name (e.g.,
195/// `"most_liquid"`).
196///
197/// # Custom algorithms
198///
199/// Use [`with_algorithm`](Self::with_algorithm) to plug in any type implementing
200/// [`Algorithm`](crate::algorithm::Algorithm) via a factory closure, bypassing the built-in
201/// registry entirely. See the `custom_algorithm` example for a full walkthrough.
202#[must_use = "a builder does nothing until .build() is called"]
203pub struct WorkerPoolBuilder {
204    config: WorkerPoolConfig,
205}
206
207impl WorkerPoolBuilder {
208    /// Create a builder with default configuration values.
209    pub fn new() -> Self {
210        Self { config: WorkerPoolConfig::default() }
211    }
212
213    /// Sets the pool name.
214    pub fn name(mut self, name: impl Into<String>) -> Self {
215        self.config.name = name.into();
216        self
217    }
218
219    /// Sets the algorithm by name (built-in registry lookup).
220    ///
221    /// Available built-in algorithms: `"most_liquid"`, `"bellman_ford"`, `"path_frank_wolfe"`,
222    /// and `"water_fill"`.
223    pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
224        self.config.spawner = AlgorithmSpawner::Registry { algorithm: algorithm.into() };
225        self
226    }
227
228    /// Sets a custom algorithm implementation via a factory closure.
229    ///
230    /// The `factory` is called once per worker thread to create an algorithm instance.
231    /// This bypasses the built-in registry, so any type implementing
232    /// [`Algorithm`](crate::algorithm::Algorithm) can be used.
233    ///
234    /// # Example
235    ///
236    /// ```ignore
237    /// builder.with_algorithm("my_algo", |config| MyAlgorithm::new(config))
238    /// ```
239    pub fn with_algorithm<A, F>(mut self, name: impl Into<String>, factory: F) -> Self
240    where
241        A: crate::algorithm::Algorithm + 'static,
242        A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
243        F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
244    {
245        let name = name.into();
246        let spawner =
247            Box::new(move |params: SpawnWorkersParams| spawn_workers_generic(params, &factory));
248        self.config.spawner = AlgorithmSpawner::Custom { algorithm: name, spawner };
249        self
250    }
251
252    /// Sets the algorithm configuration.
253    pub fn algorithm_config(mut self, config: AlgorithmConfig) -> Self {
254        self.config.algorithm_config = config;
255        self
256    }
257
258    /// Sets the number of worker threads.
259    pub fn num_workers(mut self, n: usize) -> Self {
260        self.config.num_workers = n;
261        self
262    }
263
264    /// Sets the task queue capacity.
265    pub fn task_queue_capacity(mut self, capacity: usize) -> Self {
266        self.config.task_queue_capacity = capacity;
267        self
268    }
269
270    /// Sets the policy that filters exclusive components out of each worker's graph.
271    ///
272    /// Public pools receive `Some(policy)`; exclusive-access pools (and pools of solvers with
273    /// no exclusive components configured) receive `None` and keep every component.
274    pub fn exclusivity_policy(mut self, policy: Option<ExclusivityPolicy>) -> Self {
275        self.config.exclusivity_policy = policy;
276        self
277    }
278
279    /// Builds and spawns the worker pool.
280    ///
281    /// Creates an internal task queue and returns both the worker pool and a handle
282    /// for enqueueing tasks.
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if the algorithm name is not registered.
287    pub fn build(
288        self,
289        market_data: MarketData,
290        derived_data: SharedDerivedDataRef,
291        event_rx: broadcast::Receiver<MarketEvent>,
292        derived_event_rx: broadcast::Receiver<DerivedDataEvent>,
293    ) -> Result<(WorkerPool, TaskQueueHandle), UnknownAlgorithmError> {
294        // Create task queue internally
295        let task_queue =
296            TaskQueue::new(TaskQueueConfig { capacity: self.config.task_queue_capacity });
297        let (task_handle, task_rx) = task_queue.split();
298
299        // Spawn worker pool
300        let pool = WorkerPool::spawn(
301            self.config,
302            task_rx,
303            market_data,
304            derived_data,
305            event_rx,
306            derived_event_rx,
307        )?;
308
309        Ok((pool, task_handle))
310    }
311}
312
313impl Default for WorkerPoolBuilder {
314    fn default() -> Self {
315        Self::new()
316    }
317}