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