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