1use std::{path::PathBuf, sync::Arc};
2
3use tuwunel_core::{
4 Config, Server, at, debug,
5 debug::INFO_SPAN_LEVEL,
6 debug_info, debug_warn, expected, info, is_equal_to,
7 utils::{
8 BoolExt,
9 math::usize_from_f64,
10 result::LogDebugErr,
11 stream,
12 stream::{AMPLIFICATION_LIMIT, WIDTH_LIMIT},
13 sys::{
14 cgroup_max_tasks,
15 compute::{available_parallelism, cores_available, is_core_available},
16 max_threads,
17 storage::{self, MultiDevice},
18 },
19 },
20 warn,
21};
22
23use super::{QUEUE_LIMIT, WORKER_LIMIT};
24
25#[tracing::instrument(
46 level = INFO_SPAN_LEVEL,
47 skip_all,
48 ret(level = "trace"),
49)]
50pub(super) fn configure(server: &Arc<Server>) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
51 let config = &server.config;
52 let num_cores = available_parallelism();
53
54 let cores_max = cores_available()
55 .last()
56 .unwrap_or(0)
57 .saturating_add(1);
58
59 let path: PathBuf = config.database_path.clone();
60 let device_name = storage::name_from_path(&path)
61 .log_debug_err()
62 .ok();
63
64 let devices = storage::md_discover(&path);
65 let topology_detected = devices.md.is_empty().is_false();
66 debug!(?topology_detected, ?device_name, ?devices);
67
68 let default_worker_count = topology_detected
69 .is_false()
70 .then_some(config.db_pool_workers)
71 .map(|workers| workers.saturating_mul(num_cores));
72
73 let total_tags = sum_total_tags(&devices, default_worker_count);
74 let topology = compute_topology(&devices, topology_detected, cores_max);
75 let max_workers =
76 compute_max_workers(&devices, default_worker_count, config.db_pool_max_workers);
77
78 let chan_limit = expected!(max_workers / num_cores)
79 .saturating_sub(8)
80 .saturating_add(1)
81 .next_multiple_of(8);
82
83 let workers =
84 compute_workers(&devices, config, default_worker_count, topology.len(), chan_limit);
85
86 let workers = constrain_workers(workers, max_workers);
87
88 let queues: Vec<usize> = workers
89 .iter()
90 .map(|count| {
91 count
92 .saturating_mul(config.db_pool_queue_mult)
93 .min(QUEUE_LIMIT.1)
94 })
95 .collect();
96
97 let total_workers = workers.iter().sum::<usize>();
98 let total_capacity = queues.iter().sum::<usize>();
99 let num_queues = queues.iter().filter(|&&cap| cap > 0).count();
100
101 if config.stream_width_scale > 0.0 {
102 update_stream_width(server, num_queues, total_workers, total_capacity);
103 }
104
105 log_topology(
106 topology_detected,
107 device_name.as_deref(),
108 num_cores,
109 &topology,
110 &workers,
111 &queues,
112 num_queues,
113 total_workers,
114 total_tags,
115 total_capacity,
116 );
117
118 warn_task_budget(total_workers, num_cores);
119
120 assert!(total_workers > 0, "some workers expected");
121 debug_assert!(total_workers <= max_workers.max(num_queues), "spawning too many workers");
124
125 assert!(!queues.is_empty(), "some queues expected");
126 assert!(!queues.iter().copied().all(is_equal_to!(0)), "positive queue capacity expected");
127
128 (topology, workers, queues)
129}
130
131fn sum_total_tags(devices: &MultiDevice, default_worker_count: Option<usize>) -> usize {
136 devices
137 .md
138 .iter()
139 .flat_map(|md| md.mq.iter())
140 .filter(|mq| mq.cpu_list.iter().copied().any(is_core_available))
141 .filter_map(|mq| mq.nr_tags)
142 .chain(default_worker_count)
143 .fold(0_usize, usize::saturating_add)
144}
145
146fn compute_topology(
150 devices: &MultiDevice,
151 topology_detected: bool,
152 cores_max: usize,
153) -> Vec<usize> {
154 devices
155 .md
156 .iter()
157 .flat_map(|md| md.mq.iter())
158 .fold(vec![0; cores_max], |mut topology, mq| {
159 mq.cpu_list
160 .iter()
161 .filter(|&&id| id < cores_max)
162 .filter(|&&id| is_core_available(id))
163 .for_each(|&id| {
164 topology[id] = mq.id;
165 });
166
167 topology
168 })
169 .into_iter()
170 .enumerate()
171 .map(|(core_id, queue_id)| {
172 topology_detected
173 .then_some(queue_id)
174 .unwrap_or(core_id)
175 })
176 .collect()
177}
178
179fn compute_max_workers(
183 devices: &MultiDevice,
184 default_worker_count: Option<usize>,
185 max_workers_cfg: usize,
186) -> usize {
187 let max_threads = max_threads()
188 .map(at!(0))
189 .unwrap_or(usize::MAX)
190 .saturating_div(3);
191
192 devices
193 .md
194 .iter()
195 .flat_map(|md| md.mq.iter())
196 .filter_map(|mq| mq.nr_tags)
197 .chain(default_worker_count)
198 .fold(0_usize, usize::saturating_add)
199 .min(max_workers_cfg)
200 .clamp(WORKER_LIMIT.0, max_threads)
201 .clamp(WORKER_LIMIT.0, WORKER_LIMIT.1)
202}
203
204fn compute_workers(
209 devices: &MultiDevice,
210 config: &Config,
211 default_worker_count: Option<usize>,
212 topology_len: usize,
213 chan_limit: usize,
214) -> Vec<usize> {
215 let default_workers = default_worker_count
216 .into_iter()
217 .cycle()
218 .enumerate()
219 .map(move |(core_id, count)| {
220 is_core_available(core_id)
221 .then_some(count)
222 .unwrap_or(0)
223 .min(chan_limit)
224 });
225
226 devices
227 .md
228 .iter()
229 .inspect(|md| debug!(?md))
230 .flat_map(|md| md.mq.iter())
231 .map(|mq| {
232 let shares = mq
233 .cpu_list
234 .iter()
235 .filter(|&&id| is_core_available(id))
236 .count();
237
238 let conf_limit = config
239 .db_pool_workers_limit
240 .saturating_mul(shares);
241
242 let hard_limit = devices
243 .md
244 .iter()
245 .filter(|_| shares > 0)
246 .fold(0_usize, |acc, mq| {
247 mq.nr_requests
248 .map(|nr| nr.min(conf_limit))
249 .or(Some(conf_limit))
250 .map(|nr| acc.saturating_add(nr))
251 .unwrap_or(acc)
252 });
253
254 let tags = mq
255 .nr_tags
256 .unwrap_or(WORKER_LIMIT.0)
257 .min(hard_limit)
258 .min(chan_limit);
259
260 debug!(?mq, ?shares, ?tags, ?conf_limit, ?hard_limit, ?chan_limit);
261
262 tags
263 })
264 .chain(default_workers)
265 .take(topology_len)
266 .collect()
267}
268
269fn constrain_workers(mut workers: Vec<usize>, max_workers: usize) -> Vec<usize> {
277 let total: usize = workers.iter().sum();
278
279 if total <= max_workers {
280 return workers;
281 }
282
283 let num_groups = workers.iter().filter(|&&count| count > 0).count();
284
285 let share = expected!(max_workers / num_groups);
286 let extra = expected!(max_workers % num_groups);
287
288 workers
289 .iter_mut()
290 .filter(|count| **count > 0)
291 .enumerate()
292 .for_each(|(rank, count)| {
293 *count = share
294 .saturating_add(usize::from(rank < extra))
295 .max(1)
296 .min(*count);
297 });
298
299 workers
300}
301
302#[expect(clippy::too_many_arguments)]
303fn log_topology(
304 topology_detected: bool,
305 device_name: Option<&str>,
306 num_cores: usize,
307 topology: &[usize],
308 workers: &[usize],
309 queues: &[usize],
310 num_queues: usize,
311 total_workers: usize,
312 total_tags: usize,
313 total_capacity: usize,
314) {
315 if topology_detected {
316 debug_info!(?num_cores, ?topology, ?workers, ?queues, "Frontend topology",);
317 info!(
318 device_name = ?device_name.unwrap_or("None"),
319 ?num_queues,
320 ?total_workers,
321 ?total_tags,
322 ?total_capacity,
323 stream_width = ?stream::automatic_width(),
324 amplification = ?stream::automatic_amplification(),
325 "Frontend topology",
326 );
327 } else {
328 debug_info!(?num_cores, ?topology, ?workers, ?queues, "Frontend topology (defaults)");
329 debug_warn!(
330 device_name = ?device_name.unwrap_or("None"),
331 ?total_workers,
332 ?total_capacity,
333 stream_width = ?stream::automatic_width(),
334 amplification = ?stream::automatic_amplification(),
335 "Storage hardware not detected for database directory; assuming defaults.",
336 );
337 }
338}
339
340fn warn_task_budget(total_workers: usize, num_cores: usize) {
347 const TASK_RESERVE: usize = 32;
350
351 let Some(max_tasks) = cgroup_max_tasks() else {
352 return;
353 };
354
355 let tasks = total_workers
356 .saturating_add(num_cores)
357 .saturating_add(TASK_RESERVE);
358
359 if tasks > max_tasks {
360 warn!(
361 ?total_workers,
362 ?max_tasks,
363 "The database pool may exceed this container's task limit; raise the task limit \
364 (--pids-limit for docker and podman) or lower db_pool_max_workers."
365 );
366 }
367}
368
369#[expect(clippy::as_conversions, clippy::cast_precision_loss)]
370fn update_stream_width(
371 server: &Arc<Server>,
372 num_queues: usize,
373 total_workers: usize,
374 _total_capacity: usize,
375) {
376 assert!(num_queues > 0, "Expected at least one queue.");
377 assert!(total_workers > 0, "Expected some workers.");
378
379 let config = &server.config;
380 let scale: f64 = config.stream_width_scale.min(100.0).into();
381 let max_width = expected!(total_workers / num_queues);
382
383 let old_width = stream::automatic_width();
384 let old_scale_width = expected!(old_width * num_queues);
385
386 let new_scale = total_workers as f64 / old_scale_width as f64;
387 let new_scale = new_scale.clamp(1.0, 4.0);
388 let new_scale_width = new_scale * old_width as f64;
389 let new_scale_width = usize_from_f64(new_scale_width)
390 .expect("failed to convert f64 to usize")
391 .next_multiple_of(8);
392
393 let req_width = usize_from_f64(scale * new_scale_width as f64)
394 .expect("failed to convert f64 to usize")
395 .next_multiple_of(4)
396 .min(max_width)
397 .clamp(WIDTH_LIMIT.0, WIDTH_LIMIT.1);
398
399 let req_amp = new_scale * config.stream_amplification as f64;
400 let req_amp = usize_from_f64(req_amp * scale)
401 .expect("failed to convert f64 to usize")
402 .next_multiple_of(64)
403 .clamp(AMPLIFICATION_LIMIT.0, AMPLIFICATION_LIMIT.1);
404
405 let (old_width, new_width) = stream::set_width(req_width);
406 let (old_amp, new_amp) = stream::set_amplification(req_amp);
407 debug!(
408 config_scale = ?config.stream_width_scale,
409 ?old_width,
410 ?new_scale,
411 ?new_width,
412 ?old_amp,
413 ?new_amp,
414 "Updated global stream width"
415 );
416}
417
418#[cfg(test)]
419mod tests {
420 use super::constrain_workers;
421
422 #[test]
423 fn constrain_respects_the_maximum() {
424 let constrained = constrain_workers(vec![8; 24], 64);
425
426 assert_eq!(constrained.iter().sum::<usize>(), 64);
427 assert!(constrained.iter().all(|&count| count > 0));
428 }
429
430 #[test]
431 fn constrain_floors_populated_groups() {
432 assert_eq!(constrain_workers(vec![8, 0, 8, 8], 2), vec![1, 0, 1, 1]);
433 }
434
435 #[test]
436 fn constrain_passes_a_satisfied_total() {
437 let workers = vec![4, 0, 4];
438
439 assert_eq!(constrain_workers(workers.clone(), 8), workers);
440 }
441}