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}