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