1use std::future::Future;
17use std::sync::{Mutex, Once};
18
19use common_telemetry::info;
20use once_cell::sync::Lazy;
21use paste::paste;
22use serde::{Deserialize, Serialize};
23
24use crate::runtime::{BuilderBuild, RuntimeTrait};
25use crate::{Builder, JoinHandle, Runtime};
26
27const GLOBAL_WORKERS: usize = 8;
28const COMPACT_WORKERS: usize = 4;
29const HB_WORKERS: usize = 2;
30const MIN_RUNTIME_THREADS: usize = 2;
33
34#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
36#[serde(default)]
37pub struct RuntimeOptions {
38 pub global_rt_size: usize,
40 pub compact_rt_size: usize,
42 pub compact_rt_max_blocking_threads: usize,
44 pub query_rt_size: usize,
46 pub ingest_rt_size: usize,
48}
49
50impl RuntimeOptions {
51 fn with_num_cpus(cpus: usize) -> Self {
52 let cpus = usize::max(cpus, MIN_RUNTIME_THREADS);
53 Self {
54 global_rt_size: cpus,
55 compact_rt_size: usize::max(cpus / 2, MIN_RUNTIME_THREADS),
56 compact_rt_max_blocking_threads: usize::max(cpus / 2, MIN_RUNTIME_THREADS),
57 query_rt_size: usize::max(cpus.saturating_sub(1), MIN_RUNTIME_THREADS),
58 ingest_rt_size: cpus,
59 }
60 }
61}
62
63impl Default for RuntimeOptions {
64 fn default() -> Self {
65 Self::with_num_cpus(num_cpus::get())
66 }
67}
68
69pub fn create_runtime(runtime_name: &str, thread_name: &str, worker_threads: usize) -> Runtime {
70 info!(
71 "Creating runtime with runtime_name: {runtime_name}, thread_name: {thread_name}, work_threads: {worker_threads}."
72 );
73 Builder::default()
74 .runtime_name(runtime_name)
75 .thread_name(thread_name)
76 .worker_threads(worker_threads)
77 .build()
78 .expect("Fail to create runtime")
79}
80
81fn create_compact_runtime(
82 runtime_name: &str,
83 thread_name: &str,
84 worker_threads: usize,
85 max_blocking_threads: usize,
86) -> Runtime {
87 let max_blocking_threads = max_blocking_threads.max(1);
88 info!(
89 "Creating compact runtime with runtime_name: {runtime_name}, thread_name: {thread_name}, work_threads: {worker_threads}, max_blocking_threads: {max_blocking_threads}."
90 );
91 Builder::default()
92 .runtime_name(runtime_name)
93 .thread_name(thread_name)
94 .worker_threads(worker_threads)
95 .max_blocking_threads(max_blocking_threads)
96 .build()
97 .expect("Fail to create runtime")
98}
99
100struct GlobalRuntimes {
101 global_runtime: Runtime,
102 compact_runtime: Runtime,
103 hb_runtime: Runtime,
104 query_runtime: Runtime,
105 ingest_runtime: Runtime,
106}
107
108macro_rules! define_spawn {
109 ($type: ident) => {
110 paste! {
111
112 fn [<spawn_ $type>]<F>(&self, future: F) -> JoinHandle<F::Output>
113 where
114 F: Future + Send + 'static,
115 F::Output: Send + 'static,
116 {
117 self.[<$type _runtime>].spawn(future)
118 }
119
120 fn [<spawn_blocking_ $type>]<F, R>(&self, future: F) -> JoinHandle<R>
121 where
122 F: FnOnce() -> R + Send + 'static,
123 R: Send + 'static,
124 {
125 self.[<$type _runtime>].spawn_blocking(future)
126 }
127
128 fn [<block_on_ $type>]<F: Future>(&self, future: F) -> F::Output {
129 self.[<$type _runtime>].block_on(future)
130 }
131 }
132 };
133}
134
135impl GlobalRuntimes {
136 define_spawn!(global);
137 define_spawn!(compact);
138 define_spawn!(hb);
139 define_spawn!(query);
140 define_spawn!(ingest);
141
142 fn new(
143 global: Option<Runtime>,
144 compact: Option<Runtime>,
145 heartbeat: Option<Runtime>,
146 query: Option<Runtime>,
147 ingest: Option<Runtime>,
148 ) -> Self {
149 let global_runtime =
150 global.unwrap_or_else(|| create_runtime("global", "global-worker", GLOBAL_WORKERS));
151 let query_runtime = query.unwrap_or_else(|| global_runtime.clone());
152 let ingest_runtime = ingest.unwrap_or_else(|| global_runtime.clone());
153
154 Self {
155 global_runtime,
156 compact_runtime: compact.unwrap_or_else(|| {
157 let max_blocking_threads =
158 RuntimeOptions::default().compact_rt_max_blocking_threads;
159 create_compact_runtime(
160 "compact",
161 "compact-worker",
162 COMPACT_WORKERS,
163 max_blocking_threads,
164 )
165 }),
166 hb_runtime: heartbeat
167 .unwrap_or_else(|| create_runtime("heartbeat", "hb-worker", HB_WORKERS)),
168 query_runtime,
169 ingest_runtime,
170 }
171 }
172}
173
174#[derive(Default)]
175struct ConfigRuntimes {
176 global_runtime: Option<Runtime>,
177 compact_runtime: Option<Runtime>,
178 hb_runtime: Option<Runtime>,
179 query_runtime: Option<Runtime>,
180 ingest_runtime: Option<Runtime>,
181 already_init: bool,
182}
183
184static GLOBAL_RUNTIMES: Lazy<GlobalRuntimes> = Lazy::new(|| {
185 let mut c = CONFIG_RUNTIMES.lock().unwrap();
186 let global = c.global_runtime.take();
187 let compact = c.compact_runtime.take();
188 let heartbeat = c.hb_runtime.take();
189 let query = c.query_runtime.take();
190 let ingest = c.ingest_runtime.take();
191 c.already_init = true;
192
193 GlobalRuntimes::new(global, compact, heartbeat, query, ingest)
194});
195
196static CONFIG_RUNTIMES: Lazy<Mutex<ConfigRuntimes>> =
197 Lazy::new(|| Mutex::new(ConfigRuntimes::default()));
198
199pub fn init_global_runtimes(options: &RuntimeOptions) {
205 static START: Once = Once::new();
206 START.call_once(move || {
207 let mut c = CONFIG_RUNTIMES.lock().unwrap();
208 assert!(!c.already_init, "Global runtimes already initialized");
209 c.global_runtime = Some(create_runtime(
210 "global",
211 "global-worker",
212 options.global_rt_size,
213 ));
214 c.compact_runtime = Some(create_compact_runtime(
215 "compact",
216 "compact-worker",
217 options.compact_rt_size,
218 options.compact_rt_max_blocking_threads,
219 ));
220 c.hb_runtime = Some(create_runtime("heartbeat", "hb-worker", HB_WORKERS));
221 });
222}
223
224pub fn init_datanode_runtimes(options: &RuntimeOptions) {
230 static START: Once = Once::new();
231 START.call_once(move || {
232 let mut c = CONFIG_RUNTIMES.lock().unwrap();
233 assert!(!c.already_init, "Global runtimes already initialized");
234 c.query_runtime = Some(create_runtime(
235 "query",
236 "query-worker",
237 options.query_rt_size,
238 ));
239 c.ingest_runtime = Some(create_runtime(
240 "ingest",
241 "ingest-worker",
242 options.ingest_rt_size,
243 ));
244 });
245}
246
247macro_rules! define_global_runtime_spawn {
248 ($type: ident) => {
249 paste! {
250 #[doc = "Returns the global `" $type "` thread pool."]
251 pub fn [<$type _runtime>]() -> Runtime {
252 GLOBAL_RUNTIMES.[<$type _runtime>].clone()
253 }
254
255 #[doc = "Spawn a future and execute it in `" $type "` thread pool."]
256 pub fn [<spawn_ $type>]<F>(future: F) -> JoinHandle<F::Output>
257 where
258 F: Future + Send + 'static,
259 F::Output: Send + 'static,
260 {
261 GLOBAL_RUNTIMES.[<spawn_ $type>](future)
262 }
263
264 #[doc = "Run the blocking operation in `" $type "` thread pool."]
265 pub fn [<spawn_blocking_ $type>]<F, R>(future: F) -> JoinHandle<R>
266 where
267 F: FnOnce() -> R + Send + 'static,
268 R: Send + 'static,
269 {
270 GLOBAL_RUNTIMES.[<spawn_blocking_ $type>](future)
271 }
272
273 #[doc = "Run a future to complete in `" $type "` thread pool."]
274 pub fn [<block_on_ $type>]<F: Future>(future: F) -> F::Output {
275 GLOBAL_RUNTIMES.[<block_on_ $type>](future)
276 }
277 }
278 };
279}
280
281define_global_runtime_spawn!(global);
282define_global_runtime_spawn!(compact);
283define_global_runtime_spawn!(hb);
284define_global_runtime_spawn!(query);
285define_global_runtime_spawn!(ingest);
286
287#[cfg(test)]
288mod tests {
289 use std::sync::mpsc;
290 use std::time::Duration;
291
292 use tokio_test::assert_ok;
293
294 use super::*;
295
296 #[test]
297 fn test_datanode_runtime_options_default() {
298 let options = RuntimeOptions::default();
299 let cpus = usize::max(num_cpus::get(), MIN_RUNTIME_THREADS);
300
301 assert_eq!(cpus, options.global_rt_size);
302 assert_eq!(
303 usize::max(cpus / 2, MIN_RUNTIME_THREADS),
304 options.compact_rt_size
305 );
306 assert_eq!(
307 usize::max(cpus / 2, MIN_RUNTIME_THREADS),
308 options.compact_rt_max_blocking_threads
309 );
310 assert_eq!(
311 usize::max(cpus.saturating_sub(1), MIN_RUNTIME_THREADS),
312 options.query_rt_size
313 );
314 assert_eq!(cpus, options.ingest_rt_size);
315 }
316
317 #[test]
318 fn test_runtime_options_min_threads() {
319 for cpus in [0, 1, 2] {
320 let options = RuntimeOptions::with_num_cpus(cpus);
321 assert!(
322 options.global_rt_size >= MIN_RUNTIME_THREADS,
323 "global_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
324 options.global_rt_size
325 );
326 assert!(
327 options.compact_rt_size >= MIN_RUNTIME_THREADS,
328 "compact_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
329 options.compact_rt_size
330 );
331 assert!(
332 options.compact_rt_max_blocking_threads >= MIN_RUNTIME_THREADS,
333 "compact_rt_max_blocking_threads {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
334 options.compact_rt_max_blocking_threads
335 );
336 assert!(
337 options.query_rt_size >= MIN_RUNTIME_THREADS,
338 "query_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
339 options.query_rt_size
340 );
341 assert!(
342 options.ingest_rt_size >= MIN_RUNTIME_THREADS,
343 "ingest_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
344 options.ingest_rt_size
345 );
346 }
347 }
348
349 #[test]
350 fn test_datanode_runtimes_fallback_to_global_runtime() {
351 let runtimes = GlobalRuntimes::new(
352 Some(create_runtime("test-global", "test-global-worker", 1)),
353 None,
354 None,
355 None,
356 None,
357 );
358
359 assert_eq!("test-global", runtimes.global_runtime.name());
360 assert_eq!("test-global", runtimes.query_runtime.name());
361 assert_eq!("test-global", runtimes.ingest_runtime.name());
362 }
363
364 #[test]
365 fn test_create_compact_runtime_with_zero_max_blocking_threads() {
366 let runtime = create_compact_runtime("test-compact", "test-compact-worker", 1, 0);
367 let handle = runtime.spawn_blocking(|| 1 + 1);
368
369 assert_eq!(2, runtime.block_on(handle).unwrap());
370 }
371
372 #[test]
373 fn test_compact_runtime_limits_blocking_threads() {
374 let runtime = create_compact_runtime("test-compact", "test-compact-worker", 1, 1);
375 let (first_started_tx, first_started_rx) = mpsc::channel();
376 let (release_first_tx, release_first_rx) = mpsc::channel();
377 let first = runtime.spawn_blocking(move || {
378 first_started_tx.send(()).unwrap();
379 release_first_rx.recv().unwrap();
380 });
381 first_started_rx
382 .recv_timeout(Duration::from_secs(5))
383 .unwrap();
384
385 let (second_started_tx, second_started_rx) = mpsc::channel();
386 let second = runtime.spawn_blocking(move || second_started_tx.send(()).unwrap());
387 assert!(
388 second_started_rx
389 .recv_timeout(Duration::from_secs(1))
390 .is_err()
391 );
392
393 release_first_tx.send(()).unwrap();
394 second_started_rx
395 .recv_timeout(Duration::from_secs(5))
396 .unwrap();
397 runtime.block_on(async {
398 first.await.unwrap();
399 second.await.unwrap();
400 });
401 }
402
403 #[test]
404 fn test_datanode_runtime_spawn_block_on() {
405 let handle = spawn_query(async { 1 + 1 });
406 assert_eq!(2, block_on_query(handle).unwrap());
407
408 let handle = spawn_ingest(async { 2 + 2 });
409 assert_eq!(4, block_on_ingest(handle).unwrap());
410 }
411
412 #[test]
413 fn test_spawn_block_on() {
414 let handle = spawn_global(async { 1 + 1 });
415 assert_eq!(2, block_on_global(handle).unwrap());
416
417 let handle = spawn_compact(async { 2 + 2 });
418 assert_eq!(4, block_on_compact(handle).unwrap());
419
420 let handle = spawn_hb(async { 4 + 4 });
421 assert_eq!(8, block_on_hb(handle).unwrap());
422 }
423
424 macro_rules! define_spawn_blocking_test {
425 ($type: ident) => {
426 paste! {
427 #[test]
428 fn [<test_spawn_ $type _from_blocking>]() {
429 let runtime = [<$type _runtime>]();
430 let out = runtime.block_on(async move {
431 let inner = assert_ok!(
432 [<spawn_blocking_ $type>](move || {
433 [<spawn_ $type>](async move { "hello" })
434 }).await
435 );
436
437 assert_ok!(inner.await)
438 });
439
440 assert_eq!(out, "hello")
441 }
442 }
443 };
444 }
445
446 define_spawn_blocking_test!(global);
447 define_spawn_blocking_test!(compact);
448 define_spawn_blocking_test!(hb);
449}