1use std::collections::BTreeMap;
17use std::future::Future;
18use std::num::{NonZeroU32, NonZeroUsize};
19use std::sync::{Mutex, Once};
20
21use catio::{Scheduler, SchedulerStats, TaskClass};
22use common_telemetry::{info, warn};
23use once_cell::sync::Lazy;
24use paste::paste;
25use serde::{Deserialize, Serialize};
26use tokio::runtime::Handle;
27
28use crate::metrics::register_workload_scheduler_metrics;
29use crate::runtime::{BuilderBuild, RuntimeTrait};
30use crate::{Builder, JoinHandle, Runtime};
31
32const GLOBAL_WORKERS: usize = 8;
33const COMPACT_WORKERS: usize = 4;
34const HB_WORKERS: usize = 2;
35const MIN_RUNTIME_THREADS: usize = 2;
38pub(crate) const QUERY_TASK_CLASS: TaskClass = TaskClass::new(1);
39pub(crate) const WRITE_TASK_CLASS: TaskClass = TaskClass::new(2);
40
41#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
44#[serde(default)]
45pub struct WorkloadSchedulerOptions {
46 pub enable: bool,
48 pub query_weight: NonZeroU32,
50 pub write_weight: NonZeroU32,
52 pub sample_every_polls: NonZeroUsize,
54}
55
56impl Default for WorkloadSchedulerOptions {
57 fn default() -> Self {
58 Self {
59 enable: false,
60 query_weight: NonZeroU32::new(2).unwrap(),
61 write_weight: NonZeroU32::new(8).unwrap(),
62 sample_every_polls: NonZeroUsize::new(16).unwrap(),
63 }
64 }
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
69#[serde(default)]
70pub struct RuntimeOptions {
71 pub global_rt_size: usize,
73 pub compact_rt_size: usize,
75 pub compact_rt_max_blocking_threads: usize,
77 pub query_rt_size: usize,
79 pub ingest_rt_size: usize,
81 pub experimental_workload_scheduler: WorkloadSchedulerOptions,
83}
84
85impl RuntimeOptions {
86 fn with_num_cpus(cpus: usize) -> Self {
87 let cpus = usize::max(cpus, MIN_RUNTIME_THREADS);
88 Self {
89 global_rt_size: cpus,
90 compact_rt_size: usize::max(cpus / 2, MIN_RUNTIME_THREADS),
91 compact_rt_max_blocking_threads: usize::max(cpus / 2, MIN_RUNTIME_THREADS),
92 query_rt_size: usize::max(cpus.saturating_sub(1), MIN_RUNTIME_THREADS),
93 ingest_rt_size: cpus,
94 experimental_workload_scheduler: WorkloadSchedulerOptions::default(),
95 }
96 }
97}
98
99impl Default for RuntimeOptions {
100 fn default() -> Self {
101 Self::with_num_cpus(num_cpus::get())
102 }
103}
104
105pub fn create_runtime(runtime_name: &str, thread_name: &str, worker_threads: usize) -> Runtime {
106 info!(
107 "Creating runtime with runtime_name: {runtime_name}, thread_name: {thread_name}, work_threads: {worker_threads}."
108 );
109 Builder::default()
110 .runtime_name(runtime_name)
111 .thread_name(thread_name)
112 .worker_threads(worker_threads)
113 .build()
114 .expect("Fail to create runtime")
115}
116
117fn create_compact_runtime(
118 runtime_name: &str,
119 thread_name: &str,
120 worker_threads: usize,
121 max_blocking_threads: usize,
122) -> Runtime {
123 let max_blocking_threads = max_blocking_threads.max(1);
124 info!(
125 "Creating compact runtime with runtime_name: {runtime_name}, thread_name: {thread_name}, work_threads: {worker_threads}, max_blocking_threads: {max_blocking_threads}."
126 );
127 Builder::default()
128 .runtime_name(runtime_name)
129 .thread_name(thread_name)
130 .worker_threads(worker_threads)
131 .max_blocking_threads(max_blocking_threads)
132 .build()
133 .expect("Fail to create runtime")
134}
135
136struct GlobalRuntimes {
137 global_runtime: Runtime,
138 compact_runtime: Runtime,
139 hb_runtime: Runtime,
140 query_runtime: Runtime,
141 ingest_runtime: Runtime,
142 query_handle: Handle,
143 ingest_handle: Handle,
144 workload_scheduler: Option<Scheduler>,
145}
146
147macro_rules! define_spawn {
148 ($type: ident) => {
149 paste! {
150
151 fn [<spawn_ $type>]<F>(&self, future: F) -> JoinHandle<F::Output>
152 where
153 F: Future + Send + 'static,
154 F::Output: Send + 'static,
155 {
156 self.[<$type _runtime>].spawn(future)
157 }
158
159 fn [<spawn_blocking_ $type>]<F, R>(&self, future: F) -> JoinHandle<R>
160 where
161 F: FnOnce() -> R + Send + 'static,
162 R: Send + 'static,
163 {
164 self.[<$type _runtime>].spawn_blocking(future)
165 }
166
167 fn [<block_on_ $type>]<F: Future>(&self, future: F) -> F::Output {
168 self.[<$type _runtime>].block_on(future)
169 }
170 }
171 };
172}
173
174macro_rules! define_scheduled_spawn {
175 ($type: ident, $class: ident) => {
176 paste! {
177 fn [<spawn_ $type>]<F>(&self, future: F) -> JoinHandle<F::Output>
178 where
179 F: Future + Send + 'static,
180 F::Output: Send + 'static,
181 {
182 match &self.workload_scheduler {
183 Some(scheduler) => scheduler.spawn_in_on(
184 &self.[<$type _handle>],
185 $class,
186 future,
187 ),
188 None => self.[<$type _runtime>].spawn(future),
189 }
190 }
191
192 fn [<spawn_blocking_ $type>]<F, R>(&self, future: F) -> JoinHandle<R>
193 where
194 F: FnOnce() -> R + Send + 'static,
195 R: Send + 'static,
196 {
197 self.[<$type _runtime>].spawn_blocking(future)
198 }
199
200 fn [<block_on_ $type>]<F: Future>(&self, future: F) -> F::Output {
201 self.[<$type _runtime>].block_on(future)
202 }
203 }
204 };
205}
206
207impl GlobalRuntimes {
208 define_spawn!(global);
209 define_spawn!(compact);
210 define_spawn!(hb);
211 define_scheduled_spawn!(query, QUERY_TASK_CLASS);
212 define_scheduled_spawn!(ingest, WRITE_TASK_CLASS);
213
214 fn new(
215 global: Option<Runtime>,
216 compact: Option<Runtime>,
217 heartbeat: Option<Runtime>,
218 query: Option<Runtime>,
219 ingest: Option<Runtime>,
220 workload_scheduler: Option<Scheduler>,
221 ) -> Self {
222 let global_runtime =
223 global.unwrap_or_else(|| create_runtime("global", "global-worker", GLOBAL_WORKERS));
224 let query_runtime = query.unwrap_or_else(|| global_runtime.clone());
225 let ingest_runtime = ingest.unwrap_or_else(|| global_runtime.clone());
226 let query_handle = query_runtime.handle();
227 let ingest_handle = ingest_runtime.handle();
228 Self {
229 global_runtime,
230 compact_runtime: compact.unwrap_or_else(|| {
231 let max_blocking_threads =
232 RuntimeOptions::default().compact_rt_max_blocking_threads;
233 create_compact_runtime(
234 "compact",
235 "compact-worker",
236 COMPACT_WORKERS,
237 max_blocking_threads,
238 )
239 }),
240 hb_runtime: heartbeat
241 .unwrap_or_else(|| create_runtime("heartbeat", "hb-worker", HB_WORKERS)),
242 query_runtime,
243 ingest_runtime,
244 query_handle,
245 ingest_handle,
246 workload_scheduler,
247 }
248 }
249}
250
251#[derive(Default)]
252struct ConfigRuntimes {
253 global_runtime: Option<Runtime>,
254 compact_runtime: Option<Runtime>,
255 hb_runtime: Option<Runtime>,
256 query_runtime: Option<Runtime>,
257 ingest_runtime: Option<Runtime>,
258 workload_scheduler: Option<Scheduler>,
259 already_init: bool,
260}
261
262static GLOBAL_RUNTIMES: Lazy<GlobalRuntimes> = Lazy::new(|| {
263 let mut c = CONFIG_RUNTIMES.lock().unwrap();
264 let global = c.global_runtime.take();
265 let compact = c.compact_runtime.take();
266 let heartbeat = c.hb_runtime.take();
267 let query = c.query_runtime.take();
268 let ingest = c.ingest_runtime.take();
269 let workload_scheduler = c.workload_scheduler.take();
270 c.already_init = true;
271
272 GlobalRuntimes::new(
273 global,
274 compact,
275 heartbeat,
276 query,
277 ingest,
278 workload_scheduler,
279 )
280});
281
282static CONFIG_RUNTIMES: Lazy<Mutex<ConfigRuntimes>> =
283 Lazy::new(|| Mutex::new(ConfigRuntimes::default()));
284static START: Once = Once::new();
285
286pub fn init_global_runtimes(options: &RuntimeOptions) {
291 START.call_once(|| {
292 let mut c = CONFIG_RUNTIMES.lock().unwrap();
293 assert!(!c.already_init, "Global runtimes already initialized");
294 init_common_runtimes(&mut c, options);
295 c.already_init = true;
296 });
297}
298
299pub fn init_standalone_runtimes(options: &RuntimeOptions) {
305 START.call_once(|| {
306 let mut c = CONFIG_RUNTIMES.lock().unwrap();
307 assert!(!c.already_init, "Global runtimes already initialized");
308 init_common_runtimes(&mut c, options);
309 c.workload_scheduler = Some(create_workload_scheduler(options, options.global_rt_size));
310 c.already_init = true;
311 });
312}
313
314pub fn init_datanode_runtimes(options: &RuntimeOptions) {
325 let capacity = options
326 .query_rt_size
327 .checked_add(options.ingest_rt_size)
328 .expect("datanode workload scheduler runtime capacity overflowed usize");
329 START.call_once(|| {
330 let mut c = CONFIG_RUNTIMES.lock().unwrap();
331 assert!(!c.already_init, "Global runtimes already initialized");
332 init_common_runtimes(&mut c, options);
333 c.query_runtime = Some(create_runtime(
334 "query",
335 "query-worker",
336 options.query_rt_size,
337 ));
338 c.ingest_runtime = Some(create_runtime(
339 "ingest",
340 "ingest-worker",
341 options.ingest_rt_size,
342 ));
343 c.workload_scheduler = Some(create_workload_scheduler(options, capacity));
344 c.already_init = true;
345 });
346}
347
348fn init_common_runtimes(c: &mut ConfigRuntimes, options: &RuntimeOptions) {
349 c.global_runtime = Some(create_runtime(
350 "global",
351 "global-worker",
352 options.global_rt_size,
353 ));
354 c.compact_runtime = Some(create_compact_runtime(
355 "compact",
356 "compact-worker",
357 options.compact_rt_size,
358 options.compact_rt_max_blocking_threads,
359 ));
360 c.hb_runtime = Some(create_runtime("heartbeat", "hb-worker", HB_WORKERS));
361}
362
363fn create_workload_scheduler(options: &RuntimeOptions, capacity: usize) -> Scheduler {
364 assert!(
365 capacity > 0,
366 "experimental workload scheduler capacity must be greater than zero"
367 );
368 let scheduler_options = &options.experimental_workload_scheduler;
369 let scheduler = Scheduler::builder()
370 .max_concurrent_polls(capacity)
372 .sample_every_polls(scheduler_options.sample_every_polls.get())
373 .weight(QUERY_TASK_CLASS, scheduler_options.query_weight.get())
374 .weight(WRITE_TASK_CLASS, scheduler_options.write_weight.get())
375 .build();
376 scheduler.set_enabled(scheduler_options.enable);
377 register_workload_scheduler_metrics(scheduler.clone());
378 info!(
379 "Constructed the experimental workload scheduler: internal_capacity={}, \
380 query_weight={}, write_weight={}, sample_every_polls={}, enabled={}",
381 capacity,
382 scheduler_options.query_weight,
383 scheduler_options.write_weight,
384 scheduler_options.sample_every_polls,
385 scheduler_options.enable
386 );
387 scheduler
388}
389
390macro_rules! define_global_runtime_spawn {
391 ($type: ident) => {
392 paste! {
393 #[doc = "Returns the global `" $type "` thread pool."]
394 pub fn [<$type _runtime>]() -> Runtime {
395 GLOBAL_RUNTIMES.[<$type _runtime>].clone()
396 }
397
398 #[doc = "Spawn a future and execute it in `" $type "` thread pool."]
399 pub fn [<spawn_ $type>]<F>(future: F) -> JoinHandle<F::Output>
400 where
401 F: Future + Send + 'static,
402 F::Output: Send + 'static,
403 {
404 GLOBAL_RUNTIMES.[<spawn_ $type>](future)
405 }
406
407 #[doc = "Run the blocking operation in `" $type "` thread pool."]
408 pub fn [<spawn_blocking_ $type>]<F, R>(future: F) -> JoinHandle<R>
409 where
410 F: FnOnce() -> R + Send + 'static,
411 R: Send + 'static,
412 {
413 GLOBAL_RUNTIMES.[<spawn_blocking_ $type>](future)
414 }
415
416 #[doc = "Run a future to complete in `" $type "` thread pool."]
417 pub fn [<block_on_ $type>]<F: Future>(future: F) -> F::Output {
418 GLOBAL_RUNTIMES.[<block_on_ $type>](future)
419 }
420 }
421 };
422}
423
424define_global_runtime_spawn!(global);
425define_global_runtime_spawn!(compact);
426define_global_runtime_spawn!(hb);
427define_global_runtime_spawn!(query);
428define_global_runtime_spawn!(ingest);
429
430pub fn workload_scheduler_enabled() -> bool {
433 GLOBAL_RUNTIMES
434 .workload_scheduler
435 .as_ref()
436 .is_some_and(Scheduler::is_enabled)
437}
438
439pub fn set_workload_scheduler_enabled(enabled: bool) -> bool {
442 let Some(scheduler) = GLOBAL_RUNTIMES.workload_scheduler.as_ref() else {
443 warn!(
444 "The experimental workload scheduler was not constructed at startup; ignoring enabled={enabled}"
445 );
446 return false;
447 };
448
449 scheduler.set_enabled(enabled);
450 info!("Experimental workload scheduler enabled={enabled}");
451 true
452}
453
454pub fn set_workload_scheduler_weights(query: NonZeroU32, write: NonZeroU32) -> bool {
457 let Some(scheduler) = GLOBAL_RUNTIMES.workload_scheduler.as_ref() else {
458 warn!(
459 "The experimental workload scheduler was not constructed at startup; ignoring query_weight={query}, write_weight={write}"
460 );
461 return false;
462 };
463
464 let weights = BTreeMap::from([(QUERY_TASK_CLASS, query), (WRITE_TASK_CLASS, write)]);
465 scheduler.set_weights(&weights);
466 info!("Experimental workload scheduler weights query={query}, write={write}");
467 true
468}
469
470pub fn workload_scheduler_stats() -> Option<SchedulerStats> {
473 GLOBAL_RUNTIMES
474 .workload_scheduler
475 .as_ref()
476 .map(Scheduler::stats)
477}
478
479#[cfg(test)]
480mod tests {
481 use std::future::Future;
482 use std::pin::Pin;
483 use std::sync::atomic::{AtomicBool, Ordering};
484 use std::sync::{Arc, mpsc};
485 use std::task::{Context, Poll};
486 use std::time::{Duration, Instant};
487
488 use tokio_test::assert_ok;
489
490 use super::*;
491
492 struct CooperativePolls {
493 stop: Arc<AtomicBool>,
494 }
495
496 impl Future for CooperativePolls {
497 type Output = ();
498
499 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
500 let deadline = Instant::now() + Duration::from_micros(100);
501 while Instant::now() < deadline {
502 std::hint::spin_loop();
503 }
504
505 if self.stop.load(Ordering::Relaxed) {
506 Poll::Ready(())
507 } else {
508 cx.waker().wake_by_ref();
509 Poll::Pending
510 }
511 }
512 }
513
514 fn wait_until<F>(description: &str, condition: F)
515 where
516 F: Fn() -> bool,
517 {
518 let deadline = Instant::now() + Duration::from_secs(5);
519 while !condition() {
520 assert!(
521 Instant::now() < deadline,
522 "timed out waiting for {description}"
523 );
524 std::thread::sleep(Duration::from_millis(1));
525 }
526 }
527
528 #[test]
529 fn test_datanode_runtime_options_default() {
530 let options = RuntimeOptions::default();
531 let cpus = usize::max(num_cpus::get(), MIN_RUNTIME_THREADS);
532
533 assert_eq!(cpus, options.global_rt_size);
534 assert_eq!(
535 usize::max(cpus / 2, MIN_RUNTIME_THREADS),
536 options.compact_rt_size
537 );
538 assert_eq!(
539 usize::max(cpus / 2, MIN_RUNTIME_THREADS),
540 options.compact_rt_max_blocking_threads
541 );
542 assert_eq!(
543 usize::max(cpus.saturating_sub(1), MIN_RUNTIME_THREADS),
544 options.query_rt_size
545 );
546 assert_eq!(cpus, options.ingest_rt_size);
547 assert_eq!(
548 WorkloadSchedulerOptions::default(),
549 options.experimental_workload_scheduler
550 );
551 }
552
553 #[test]
554 fn test_runtime_options_min_threads() {
555 for cpus in [0, 1, 2] {
556 let options = RuntimeOptions::with_num_cpus(cpus);
557 assert!(
558 options.global_rt_size >= MIN_RUNTIME_THREADS,
559 "global_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
560 options.global_rt_size
561 );
562 assert!(
563 options.compact_rt_size >= MIN_RUNTIME_THREADS,
564 "compact_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
565 options.compact_rt_size
566 );
567 assert!(
568 options.compact_rt_max_blocking_threads >= MIN_RUNTIME_THREADS,
569 "compact_rt_max_blocking_threads {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
570 options.compact_rt_max_blocking_threads
571 );
572 assert!(
573 options.query_rt_size >= MIN_RUNTIME_THREADS,
574 "query_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
575 options.query_rt_size
576 );
577 assert!(
578 options.ingest_rt_size >= MIN_RUNTIME_THREADS,
579 "ingest_rt_size {} < {MIN_RUNTIME_THREADS} with {cpus} cpus",
580 options.ingest_rt_size
581 );
582 }
583 }
584
585 #[test]
586 fn test_datanode_runtimes_fallback_to_global_runtime() {
587 let runtimes = GlobalRuntimes::new(
588 Some(create_runtime("test-global", "test-global-worker", 1)),
589 None,
590 None,
591 None,
592 None,
593 None,
594 );
595
596 assert_eq!("test-global", runtimes.global_runtime.name());
597 assert_eq!("test-global", runtimes.query_runtime.name());
598 assert_eq!("test-global", runtimes.ingest_runtime.name());
599 }
600
601 #[test]
602 fn test_create_compact_runtime_with_zero_max_blocking_threads() {
603 let runtime = create_compact_runtime("test-compact", "test-compact-worker", 1, 0);
604 let handle = runtime.spawn_blocking(|| 1 + 1);
605
606 assert_eq!(2, runtime.block_on(handle).unwrap());
607 }
608
609 #[test]
610 fn test_compact_runtime_limits_blocking_threads() {
611 let runtime = create_compact_runtime("test-compact", "test-compact-worker", 1, 1);
612 let (first_started_tx, first_started_rx) = mpsc::channel();
613 let (release_first_tx, release_first_rx) = mpsc::channel();
614 let first = runtime.spawn_blocking(move || {
615 first_started_tx.send(()).unwrap();
616 release_first_rx.recv().unwrap();
617 });
618 first_started_rx
619 .recv_timeout(Duration::from_secs(5))
620 .unwrap();
621
622 let (second_started_tx, second_started_rx) = mpsc::channel();
623 let second = runtime.spawn_blocking(move || second_started_tx.send(()).unwrap());
624 assert!(
625 second_started_rx
626 .recv_timeout(Duration::from_secs(1))
627 .is_err()
628 );
629
630 release_first_tx.send(()).unwrap();
631 second_started_rx
632 .recv_timeout(Duration::from_secs(5))
633 .unwrap();
634 runtime.block_on(async {
635 first.await.unwrap();
636 second.await.unwrap();
637 });
638 }
639
640 #[test]
641 fn test_workload_scheduler_builds_with_initial_enabled_state() {
642 let mut options = RuntimeOptions::default();
643 options.experimental_workload_scheduler.enable = false;
644 options.experimental_workload_scheduler.sample_every_polls = NonZeroUsize::new(7).unwrap();
645 let scheduler = create_workload_scheduler(&options, options.global_rt_size);
646 assert_eq!(7, scheduler.stats().sample_every_polls);
647 assert!(!scheduler.is_enabled());
648
649 scheduler.set_enabled(true);
650 assert!(scheduler.is_enabled());
651 }
652
653 #[test]
654 fn test_workload_scheduler_bypasses_disabled_query_and_write_spawns() {
655 let runtime = create_runtime("test-workload-bypass", "test-workload-bypass-worker", 2);
656 let scheduler = Scheduler::builder()
657 .max_concurrent_polls(2)
658 .weight(QUERY_TASK_CLASS, 2)
659 .weight(WRITE_TASK_CLASS, 8)
660 .build();
661 scheduler.set_enabled(false);
662 let runtimes = GlobalRuntimes::new(
663 Some(runtime.clone()),
664 Some(runtime.clone()),
665 Some(runtime.clone()),
666 Some(runtime.clone()),
667 Some(runtime.clone()),
668 Some(scheduler.clone()),
669 );
670
671 let query = runtimes.spawn_query(async { "query" });
672 let write = runtimes.spawn_ingest(async { "write" });
673 let (query, write) =
674 runtime.block_on(async { (query.await.unwrap(), write.await.unwrap()) });
675
676 assert_eq!("query", query);
677 assert_eq!("write", write);
678 let stats = scheduler.stats();
679 for class in [QUERY_TASK_CLASS, WRITE_TASK_CLASS] {
680 let class_stats = &stats.classes[&class];
681 assert_eq!(0, class_stats.tasks);
682 assert_eq!(0, class_stats.admitted);
683 assert_eq!(0, class_stats.polls);
684 }
685 }
686
687 #[test]
688 fn test_workload_scheduler_wraps_query_and_write_spawns() {
689 let runtime = create_runtime("test-workload", "test-workload-worker", 2);
690 let scheduler = Scheduler::builder()
691 .max_concurrent_polls(2)
692 .weight(QUERY_TASK_CLASS, 2)
693 .weight(WRITE_TASK_CLASS, 8)
694 .build();
695 let runtimes = GlobalRuntimes::new(
696 Some(runtime.clone()),
697 Some(runtime.clone()),
698 Some(runtime.clone()),
699 Some(runtime.clone()),
700 Some(runtime.clone()),
701 Some(scheduler.clone()),
702 );
703
704 let query = runtimes.spawn_query(async { "query" });
705 let write = runtimes.spawn_ingest(async { "write" });
706 let (query, write) =
707 runtime.block_on(async { (query.await.unwrap(), write.await.unwrap()) });
708
709 assert_eq!("query", query);
710 assert_eq!("write", write);
711 let stats = scheduler.stats();
712 assert_eq!(1, stats.classes[&QUERY_TASK_CLASS].polls);
713 assert_eq!(1, stats.classes[&WRITE_TASK_CLASS].polls);
714 }
715
716 #[test]
717 fn test_datanode_query_backlog_does_not_starve_ingest() {
718 let query_runtime = create_runtime("test-datanode-query", "test-query-worker", 1);
719 let ingest_runtime = create_runtime("test-datanode-ingest", "test-ingest-worker", 1);
720 let scheduler = Scheduler::builder()
721 .max_concurrent_polls(2)
722 .sample_every_polls(16)
723 .weight(QUERY_TASK_CLASS, 2)
724 .weight(WRITE_TASK_CLASS, 8)
725 .build();
726 let runtimes = GlobalRuntimes::new(
727 Some(query_runtime.clone()),
728 Some(query_runtime.clone()),
729 Some(query_runtime.clone()),
730 Some(query_runtime),
731 Some(ingest_runtime),
732 Some(scheduler.clone()),
733 );
734
735 let stop_queries = Arc::new(AtomicBool::new(false));
736 let query_tasks = (0..3)
737 .map(|_| {
738 runtimes.spawn_query(CooperativePolls {
739 stop: stop_queries.clone(),
740 })
741 })
742 .collect::<Vec<_>>();
743
744 wait_until("two active query polls and a queued query", || {
747 let stats = scheduler.stats();
748 stats.active_polls == 2
749 && stats
750 .classes
751 .get(&QUERY_TASK_CLASS)
752 .is_some_and(|class| class.tasks == 3 && class.queued >= 1)
753 });
754
755 let write = runtimes.spawn_ingest(async {});
756 wait_until("write body", || write.is_finished());
757
758 stop_queries.store(true, Ordering::Relaxed);
759 for query in &query_tasks {
760 query.abort();
761 }
762 runtimes.block_on_query(async {
763 for query in query_tasks {
764 let _ = query.await;
765 }
766 });
767 runtimes.block_on_ingest(async {
768 write.await.unwrap();
769 });
770 wait_until("scheduler polls to drain", || {
771 scheduler.stats().active_polls == 0
772 });
773 }
774
775 #[test]
776 fn test_datanode_runtime_spawn_block_on() {
777 let handle = spawn_query(async { 1 + 1 });
778 assert_eq!(2, block_on_query(handle).unwrap());
779
780 let handle = spawn_ingest(async { 2 + 2 });
781 assert_eq!(4, block_on_ingest(handle).unwrap());
782 }
783
784 #[test]
785 fn test_spawn_block_on() {
786 let handle = spawn_global(async { 1 + 1 });
787 assert_eq!(2, block_on_global(handle).unwrap());
788
789 let handle = spawn_compact(async { 2 + 2 });
790 assert_eq!(4, block_on_compact(handle).unwrap());
791
792 let handle = spawn_hb(async { 4 + 4 });
793 assert_eq!(8, block_on_hb(handle).unwrap());
794 }
795
796 macro_rules! define_spawn_blocking_test {
797 ($type: ident) => {
798 paste! {
799 #[test]
800 fn [<test_spawn_ $type _from_blocking>]() {
801 let runtime = [<$type _runtime>]();
802 let out = runtime.block_on(async move {
803 let inner = assert_ok!(
804 [<spawn_blocking_ $type>](move || {
805 [<spawn_ $type>](async move { "hello" })
806 }).await
807 );
808
809 assert_ok!(inner.await)
810 });
811
812 assert_eq!(out, "hello")
813 }
814 }
815 };
816 }
817
818 define_spawn_blocking_test!(global);
819 define_spawn_blocking_test!(compact);
820 define_spawn_blocking_test!(hb);
821}