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 exclusivity::ExclusivityPolicy,
22 market_data::MarketData,
23 },
24 graph::EdgeWeightUpdaterWithDerived,
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};
34
35/// Configuration for the worker pool.
36#[derive(Debug)]
37pub struct WorkerPoolConfig {
38 /// Human-readable name for this pool (used in logging/metrics).
39 /// Can differ from algorithm to distinguish pools with same algorithm but different configs.
40 name: String,
41 /// How to spawn workers — either a built-in registry lookup or a custom factory.
42 spawner: AlgorithmSpawner,
43 /// Number of worker threads.
44 num_workers: usize,
45 /// Configuration for the algorithm used by each worker.
46 algorithm_config: AlgorithmConfig,
47 /// Task queue capacity (maximum number of pending tasks).
48 task_queue_capacity: usize,
49 /// When set, exclusive components are filtered out of this pool's workers' graphs
50 /// (default: `None`, no filtering).
51 ///
52 /// `All` is safe as the default because it only applies when no `ExclusivityPolicy` is
53 /// configured — meaning no exclusive components exist to exclude. When a policy is set,
54 /// `FyndBuilder::assemble_components` always constructs
55 /// `Some(policy)` for `Public`-scoped pools, so this default is never relied on in
56 /// that path.
57 exclusivity_policy: Option<ExclusivityPolicy>,
58}
59
60impl WorkerPoolConfig {
61 /// Returns the algorithm name for this pool.
62 pub fn algorithm_name(&self) -> &str {
63 self.spawner.algorithm_name()
64 }
65}
66
67impl Default for WorkerPoolConfig {
68 fn default() -> Self {
69 Self {
70 name: DEFAULT_ALGORITHM.to_string(),
71 spawner: AlgorithmSpawner::Registry { algorithm: DEFAULT_ALGORITHM.to_string() },
72 num_workers: num_cpus::get(),
73 algorithm_config: AlgorithmConfig::default(),
74 task_queue_capacity: 1000,
75 exclusivity_policy: None,
76 }
77 }
78}
79
80/// A pool of worker threads for processing solve tasks.
81///
82/// Each pool is dedicated to a specific algorithm. Workers in the pool
83/// compete for tasks from the shared queue.
84pub struct WorkerPool {
85 /// Human-readable name for this pool.
86 name: String,
87 /// Algorithm name for this pool.
88 algorithm: String,
89 /// Handles to worker threads.
90 workers: Vec<JoinHandle<()>>,
91 /// Shutdown signal sender.
92 shutdown_tx: broadcast::Sender<()>,
93}
94
95impl WorkerPool {
96 /// Spawns a new worker pool.
97 ///
98 /// # Arguments
99 ///
100 /// * `config` - Worker pool configuration
101 /// * `task_rx` - Receiver for tasks from the queue
102 /// * `market_data` - Shared market data reference
103 /// * `derived_data` - Shared derived data reference (pool depths, token prices)
104 /// * `event_rx` - Broadcast receiver for market events (workers subscribe to this)
105 /// * `derived_event_rx` - Broadcast receiver for derived data events (resubscribed per worker)
106 ///
107 /// # Errors
108 ///
109 /// Returns an error if the algorithm name in config is not registered.
110 pub fn spawn(
111 config: WorkerPoolConfig,
112 task_rx: async_channel::Receiver<SolveTask>,
113 market_data: MarketData,
114 derived_data: SharedDerivedDataRef,
115 event_rx: broadcast::Receiver<MarketEvent>,
116 derived_event_rx: broadcast::Receiver<DerivedDataEvent>,
117 ) -> Result<Self, UnknownAlgorithmError> {
118 let (shutdown_tx, _) = broadcast::channel(1);
119 let name = config.name.clone();
120 let algorithm = config
121 .spawner
122 .algorithm_name()
123 .to_string();
124
125 // Spawn workers
126 let exclusivity_policy = config.exclusivity_policy.clone();
127 let params = SpawnWorkersParams {
128 algorithm: algorithm.clone(),
129 pool_name: name.clone(),
130 num_workers: config.num_workers,
131 algorithm_config: config.algorithm_config,
132 task_rx,
133 market_data,
134 derived_data,
135 event_rx,
136 derived_event_rx,
137 shutdown_tx: shutdown_tx.clone(),
138 exclusivity_policy,
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 pool name.
153 pub fn name(&self) -> &str {
154 &self.name
155 }
156
157 /// Returns the algorithm name for this 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 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 the policy that filters exclusive components out of each worker's graph.
271 ///
272 /// Public pools receive `Some(policy)`; exclusive-access pools (and pools of solvers with
273 /// no exclusive components configured) receive `None` and keep every component.
274 pub fn exclusivity_policy(mut self, policy: Option<ExclusivityPolicy>) -> Self {
275 self.config.exclusivity_policy = policy;
276 self
277 }
278
279 /// Builds and spawns the worker pool.
280 ///
281 /// Creates an internal task queue and returns both the worker pool and a handle
282 /// for enqueueing tasks.
283 ///
284 /// # Errors
285 ///
286 /// Returns an error if the algorithm name is not registered.
287 pub fn build(
288 self,
289 market_data: MarketData,
290 derived_data: SharedDerivedDataRef,
291 event_rx: broadcast::Receiver<MarketEvent>,
292 derived_event_rx: broadcast::Receiver<DerivedDataEvent>,
293 ) -> Result<(WorkerPool, TaskQueueHandle), UnknownAlgorithmError> {
294 // Create task queue internally
295 let task_queue =
296 TaskQueue::new(TaskQueueConfig { capacity: self.config.task_queue_capacity });
297 let (task_handle, task_rx) = task_queue.split();
298
299 // Spawn worker pool
300 let pool = WorkerPool::spawn(
301 self.config,
302 task_rx,
303 market_data,
304 derived_data,
305 event_rx,
306 derived_event_rx,
307 )?;
308
309 Ok((pool, task_handle))
310 }
311}
312
313impl Default for WorkerPoolBuilder {
314 fn default() -> Self {
315 Self::new()
316 }
317}