Skip to main content

common_runtime/
global.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Global runtimes
16use 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;
30/// The minimum number of worker threads for runtimes sized by CPU count.
31/// A single-threaded runtime can easily deadlock in async code.
32const MIN_RUNTIME_THREADS: usize = 2;
33
34/// The options for the global runtimes.
35#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
36#[serde(default)]
37pub struct RuntimeOptions {
38    /// The number of threads for the global default runtime.
39    pub global_rt_size: usize,
40    /// The number of threads to execute the runtime for compact operations.
41    pub compact_rt_size: usize,
42    /// The maximum number of blocking threads for compact operations.
43    pub compact_rt_max_blocking_threads: usize,
44    /// The number of threads to execute datanode query operations.
45    pub query_rt_size: usize,
46    /// The number of threads to execute datanode ingestion operations.
47    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
199/// Initialize the global runtimes
200///
201/// # Panics
202/// Panics when the global runtimes are already initialized.
203/// You should call this function before using any runtime functions.
204pub 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
224/// Initialize the datanode-specific global runtimes.
225///
226/// # Panics
227/// Panics when the global runtimes are already initialized.
228/// You should call this function before using any runtime functions.
229pub 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}