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}