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