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    /// PropAMMRouter fee tiers, shared with the fetcher that refreshes them.
54    fallback_fee_tiers: SharedFeeTiers,
55}
56
57impl WorkerPoolConfig {
58    /// Returns the algorithm name for this worker pool.
59    pub fn algorithm_name(&self) -> &str {
60        self.spawner.algorithm_name()
61    }
62}
63
64impl Default for WorkerPoolConfig {
65    fn default() -> Self {
66        Self {
67            name: DEFAULT_ALGORITHM.to_string(),
68            spawner: AlgorithmSpawner::Registry { algorithm: DEFAULT_ALGORITHM.to_string() },
69            num_workers: num_cpus::get(),
70            algorithm_config: AlgorithmConfig::default(),
71            task_queue_capacity: 1000,
72            liquidity_scope: LiquidityScope::default(),
73            fallback_fee_tiers: SharedFeeTiers::default(),
74        }
75    }
76}
77
78/// A pool of worker threads for processing solve tasks.
79///
80/// Each worker pool is dedicated to a specific algorithm. Workers in the pool
81/// compete for tasks from the shared queue.
82pub struct WorkerPool {
83    /// Human-readable name for this worker pool.
84    name: String,
85    /// Algorithm name for this worker pool.
86    algorithm: String,
87    /// Handles to worker threads.
88    workers: Vec<JoinHandle<()>>,
89    /// Shutdown signal sender.
90    shutdown_tx: broadcast::Sender<()>,
91}
92
93impl WorkerPool {
94    /// Spawns a new worker pool.
95    ///
96    /// # Arguments
97    ///
98    /// * `config` - Worker pool configuration
99    /// * `task_rx` - Receiver for tasks from the queue
100    /// * `market_data` - Shared market data reference
101    /// * `derived_data` - Shared derived data reference (component depths, token prices)
102    /// * `event_rx` - Broadcast receiver for market events (workers subscribe to this)
103    /// * `derived_event_rx` - Broadcast receiver for derived data events (resubscribed per worker)
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the algorithm name in config is not registered.
108    pub fn spawn(
109        config: WorkerPoolConfig,
110        task_rx: async_channel::Receiver<SolveTask>,
111        market_data: MarketData,
112        derived_data: SharedDerivedDataRef,
113        event_rx: broadcast::Receiver<MarketEvent>,
114        derived_event_rx: broadcast::Receiver<DerivedDataEvent>,
115    ) -> Result<Self, UnknownAlgorithmError> {
116        let (shutdown_tx, _) = broadcast::channel(1);
117        let name = config.name.clone();
118        let algorithm = config
119            .spawner
120            .algorithm_name()
121            .to_string();
122
123        // Spawn workers
124        let liquidity_scope = config.liquidity_scope;
125        let fallback_fee_tiers = config.fallback_fee_tiers.clone();
126        let params = SpawnWorkersParams {
127            algorithm: algorithm.clone(),
128            pool_name: name.clone(),
129            num_workers: config.num_workers,
130            algorithm_config: config.algorithm_config,
131            task_rx,
132            market_data,
133            derived_data,
134            event_rx,
135            derived_event_rx,
136            shutdown_tx: shutdown_tx.clone(),
137            liquidity_scope,
138            fallback_fee_tiers,
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 worker pool name.
153    pub fn name(&self) -> &str {
154        &self.name
155    }
156
157    /// Returns the algorithm name for this worker 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 worker 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 which liquidity this pool's workers ingest.
271    pub fn liquidity_scope(mut self, scope: LiquidityScope) -> Self {
272        self.config.liquidity_scope = scope;
273        self
274    }
275
276    /// Sets the PropAMMRouter fee tiers this pool's workers read.
277    pub fn fallback_fee_tiers(mut self, fallback_fee_tiers: SharedFeeTiers) -> Self {
278        self.config.fallback_fee_tiers = fallback_fee_tiers;
279        self
280    }
281
282    /// Builds and spawns the worker pool.
283    ///
284    /// Creates an internal task queue and returns both the worker pool and a handle
285    /// for enqueueing tasks.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the algorithm name is not registered.
290    pub fn build(
291        self,
292        market_data: MarketData,
293        derived_data: SharedDerivedDataRef,
294        event_rx: broadcast::Receiver<MarketEvent>,
295        derived_event_rx: broadcast::Receiver<DerivedDataEvent>,
296    ) -> Result<(WorkerPool, TaskQueueHandle), UnknownAlgorithmError> {
297        // Create task queue internally
298        let task_queue =
299            TaskQueue::new(TaskQueueConfig { capacity: self.config.task_queue_capacity });
300        let (task_handle, task_rx) = task_queue.split();
301
302        // Spawn worker pool
303        let pool = WorkerPool::spawn(
304            self.config,
305            task_rx,
306            market_data,
307            derived_data,
308            event_rx,
309            derived_event_rx,
310        )?;
311
312        Ok((pool, task_handle))
313    }
314}
315
316impl Default for WorkerPoolBuilder {
317    fn default() -> Self {
318        Self::new()
319    }
320}