1use super::CircuitConfig;
5use super::dbsp_handle::{Layout, Mode};
6use crate::SchedulerError;
7use crate::circuit::checkpointer::Checkpointer;
8use crate::circuit::dbsp_handle::StepSize;
9use crate::error::Error as DbspError;
10use crate::operator::communication::{Exchange, ExchangeActivity};
11use crate::storage::backend::StorageBackend;
12use crate::storage::file::format::Compression;
13use crate::storage::file::writer::Parameters;
14use crate::utils::process_rss_bytes;
15use crate::{
16 DetailedError,
17 storage::{
18 backend::StorageError,
19 buffer_cache::{BufferCache, build_buffer_caches},
20 dirlock::LockedDirectory,
21 },
22};
23use core_affinity::{CoreId, get_core_ids};
24use crossbeam::sync::{Parker, Unparker};
25use enum_map::{Enum, EnumMap, enum_map};
26use feldera_buffer_cache::ThreadType;
27use feldera_samply::{LongSpanBuilder, Span};
28use feldera_storage::fbuf::FBuf;
29use feldera_storage::fbuf::slab::{FBufSlabs, FBufSlabsStats, set_thread_slab_pool};
30use feldera_types::config::{DevTweaks, StorageCompression, StorageConfig, StorageOptions};
31use feldera_types::memory_pressure::{
32 CRITICAL_MEMORY_PRESSURE_THRESHOLD, HIGH_MEMORY_PRESSURE_THRESHOLD,
33 MODERATE_MEMORY_PRESSURE_THRESHOLD, MemoryPressure,
34};
35use indexmap::IndexSet;
36use once_cell::sync::Lazy;
37use serde::Serialize;
38use std::convert::identity;
39use std::iter::repeat;
40use std::net::TcpListener;
41use std::ops::{Index, Range};
42use std::path::Path;
43use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize};
44use std::sync::{LazyLock, Mutex};
45use std::time::Duration;
46use std::{
47 backtrace::Backtrace,
48 borrow::Cow,
49 cell::{Cell, RefCell},
50 collections::HashSet,
51 error::Error as StdError,
52 fmt,
53 fmt::{Debug, Display, Error as FmtError, Formatter},
54 panic::{self, Location, PanicHookInfo},
55 sync::{
56 Arc, RwLock, Weak,
57 atomic::{AtomicBool, Ordering},
58 },
59 thread::{Builder, JoinHandle, Result as ThreadResult},
60};
61use tokio::runtime::Builder as TokioBuilder;
62use tokio::runtime::Runtime as TokioRuntime;
63use tokio::sync::Notify;
64use tokio_util::sync::CancellationToken;
65use tracing::{debug, error, info, warn};
66use typedmap::TypedDashMap;
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
69pub enum Error {
70 UnknownPersistentId(String),
72 WorkerPanic {
74 panic_info: Vec<(usize, ThreadType, WorkerPanicInfo)>,
77 },
78 IncompatibleStorage,
80 CheckpointParseError(String),
82 BootstrapCircuit(String),
86 Terminated,
87}
88
89impl DetailedError for Error {
90 fn error_code(&self) -> Cow<'static, str> {
91 match self {
92 Self::UnknownPersistentId(_) => Cow::from("UnknownPersistentId"),
93 Self::WorkerPanic { .. } => Cow::from("WorkerPanic"),
94 Self::Terminated => Cow::from("Terminated"),
95 Self::IncompatibleStorage => Cow::from("IncompatibleStorage"),
96 Self::CheckpointParseError(_) => Cow::from("CheckpointParseError"),
97 Self::BootstrapCircuit(_) => Cow::from("BootstrapCircuit"),
98 }
99 }
100}
101
102impl Display for Error {
103 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
104 match self {
105 Self::UnknownPersistentId(persistent_id) => {
106 write!(f, "Unknown persistent node id: {persistent_id}")
107 }
108 Self::WorkerPanic { panic_info } => {
109 writeln!(f, "One or more worker threads terminated unexpectedly")?;
110
111 for (worker, thread_type, worker_panic_info) in panic_info.iter() {
112 writeln!(f, "{thread_type} worker thread {worker} panicked")?;
113 writeln!(f, "{worker_panic_info}")?;
114 }
115 Ok(())
116 }
117 Self::Terminated => f.write_str("circuit has been terminated"),
118 Self::IncompatibleStorage => {
119 f.write_str("Supplied storage directory does not fit the runtime circuit")
120 }
121 Self::CheckpointParseError(error) => {
122 write!(f, "Error deserializing checkpointed state: {error}")
123 }
124 Self::BootstrapCircuit(error) => {
125 write!(f, "Bootstrap circuit error: {error}")
126 }
127 }
128 }
129}
130
131impl StdError for Error {}
132
133thread_local! {
135 static RUNTIME: RefCell<Option<Runtime>> = const { RefCell::new(None) };
138
139 static CURRENT_THREAD_TYPE: Cell<Option<ThreadType>> = const { Cell::new(None) };
142
143}
144
145thread_local! {
147 static WORKER_INDEX: Cell<usize> = const { Cell::new(0) };
151
152 static BUFFER_CACHE: RefCell<Option<Arc<BufferCache>>> = const { RefCell::new(None) };
154}
155
156tokio::task_local! {
158 pub(crate) static TOKIO_WORKER_INDEX: usize;
162
163 pub(crate) static TOKIO_BUFFER_CACHE: Arc<BufferCache>;
167}
168
169pub(crate) fn current_thread_type() -> Option<ThreadType> {
170 CURRENT_THREAD_TYPE.get()
171}
172
173fn set_current_thread_type(thread_type: ThreadType) {
174 CURRENT_THREAD_TYPE.set(Some(thread_type));
175}
176
177pub struct LocalStoreMarker;
178
179pub type LocalStore = TypedDashMap<LocalStoreMarker>;
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184pub struct PanicLocation {
185 file: String,
186 line: u32,
187 col: u32,
188}
189
190impl Display for PanicLocation {
191 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
192 write!(f, "{}:{}:{}", self.file, self.line, self.col)
193 }
194}
195
196impl PanicLocation {
197 fn new(loc: &Location) -> Self {
198 Self {
199 file: loc.file().to_string(),
200 line: loc.line(),
201 col: loc.column(),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
208pub struct WorkerPanicInfo {
209 message: Option<String>,
211 location: Option<PanicLocation>,
213 backtrace: String,
215}
216
217impl Display for WorkerPanicInfo {
218 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
219 if let Some(message) = &self.message {
220 writeln!(f, "panic message: {message}")?;
221 } else {
222 writeln!(f, "panic message (none)")?;
223 }
224
225 if let Some(location) = &self.location {
226 writeln!(f, "panic location: {location}")?;
227 } else {
228 writeln!(f, "panic location: unknown")?;
229 }
230 writeln!(f, "stack trace:\n{}", self.backtrace)
231 }
232}
233
234impl WorkerPanicInfo {
235 fn new(panic_info: &PanicHookInfo) -> Self {
236 #[allow(clippy::manual_map)]
237 let message = if let Some(v) = panic_info.payload().downcast_ref::<String>() {
238 Some(v.clone())
239 } else if let Some(v) = panic_info.payload().downcast_ref::<&str>() {
240 Some(v.to_string())
241 } else {
242 None
243 };
244 let backtrace = Backtrace::force_capture().to_string();
245 let location = panic_info
246 .location()
247 .map(|location| PanicLocation::new(location));
248
249 Self {
250 message,
251 location,
252 backtrace,
253 }
254 }
255}
256
257#[derive(derive_more::Debug)]
258struct RuntimeStorage {
259 pub config: StorageConfig,
261
262 pub options: StorageOptions,
264
265 #[debug(skip)]
267 pub backend: Arc<dyn StorageBackend>,
268
269 #[allow(dead_code)]
271 locked_directory: LockedDirectory,
272}
273
274struct RuntimeInner {
275 layout: Layout,
276 mode: Mode,
277 step_size: StepSize,
278 allow_input_during_commit: bool,
279 dev_tweaks: DevTweaks,
280
281 max_rss: Option<u64>,
283
284 process_rss: AtomicU64,
286
287 memory_pressure: AtomicU8,
290
291 memory_pressure_epoch: AtomicU64,
293
294 memory_pressure_notify: Arc<Notify>,
296
297 storage: Option<RuntimeStorage>,
298 store: LocalStore,
299 kill_signal: AtomicBool,
300 cancellation_token: CancellationToken,
301 aux_threads: Mutex<Vec<(JoinHandle<()>, Unparker)>>,
302 buffer_caches: Vec<EnumMap<ThreadType, Arc<BufferCache>>>,
303
304 pin_cpus_fg: Vec<CoreId>,
306
307 pin_cpus_bg: Vec<CoreId>,
309 fbuf_slab_allocators: Vec<EnumMap<ThreadType, Arc<FBufSlabs>>>,
310 worker_sequence_numbers: Vec<AtomicUsize>,
311 panic_info: Vec<EnumMap<ThreadType, RwLock<Option<WorkerPanicInfo>>>>,
313 panicked: AtomicBool,
314
315 tokio_merger_runtime: Mutex<Option<TokioRuntime>>,
317
318 exchange_listener: Mutex<Option<TcpListener>>,
327}
328
329impl Drop for RuntimeInner {
330 fn drop(&mut self) {
331 debug!("dropping RuntimeInner");
332 }
333}
334
335impl Debug for RuntimeInner {
336 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
337 f.debug_struct("RuntimeInner")
338 .field("layout", &self.layout)
339 .field("storage", &self.storage)
340 .finish()
341 }
342}
343
344fn display_core_ids<'a>(iter: impl Iterator<Item = &'a CoreId>) -> String {
345 format!(
346 "{:?}",
347 iter.map(|core| core.id).collect::<Vec<_>>().as_slice()
348 )
349}
350
351fn map_pin_cpus(config: &CircuitConfig) -> (Vec<CoreId>, Vec<CoreId>) {
355 if config.layout.is_multihost() {
356 if !config.pin_cpus.is_empty() {
357 warn!("CPU pinning not yet supported with multihost DBSP");
358 }
359 return (Vec::new(), Vec::new());
360 }
361
362 let merger_threads = config.num_merger_threads();
363
364 let nworkers = config.layout.n_workers();
365 let pin_cpus = config
366 .pin_cpus
367 .iter()
368 .copied()
369 .map(|id| CoreId { id })
370 .collect::<IndexSet<_>>();
371 let expected_cpus = nworkers + merger_threads;
372 if pin_cpus.len() < expected_cpus {
373 if !pin_cpus.is_empty() {
374 warn!(
375 "ignoring CPU pinning request because {nworkers} foreground workers and {} merger workers require {} pinned CPUs but only {} were specified",
376 merger_threads,
377 expected_cpus,
378 pin_cpus.len()
379 )
380 }
381 return (Vec::new(), Vec::new());
382 }
383
384 let Some(core_ids) = get_core_ids() else {
385 warn!(
386 "ignoring CPU pinning request because this system's core ids list could not be obtained"
387 );
388 return (Vec::new(), Vec::new());
389 };
390 let core_ids = core_ids.iter().copied().collect::<IndexSet<_>>();
391
392 let missing_cpus = pin_cpus.difference(&core_ids).copied().collect::<Vec<_>>();
393 if !missing_cpus.is_empty() {
394 warn!(
395 "ignoring CPU pinning request because requested CPUs {missing_cpus:?} are not available (available CPUs are: {})",
396 display_core_ids(core_ids.iter())
397 );
398 return (Vec::new(), Vec::new());
399 }
400
401 let fg_cpus = &pin_cpus[0..nworkers];
402 let bg_cpus = &pin_cpus[nworkers..expected_cpus];
403 info!(
404 "pinning foreground workers to CPUs {} and background workers to CPUs {}",
405 display_core_ids(fg_cpus.iter()),
406 display_core_ids(bg_cpus.iter())
407 );
408 let fg_pinning = (0..nworkers).map(|i| fg_cpus[i]).collect();
409
410 let bg_pinning = (0..merger_threads).map(|i| bg_cpus[i]).collect();
411
412 (fg_pinning, bg_pinning)
413}
414
415impl RuntimeInner {
416 fn new(config: CircuitConfig) -> Result<Self, DbspError> {
417 let nworkers = config.layout.local_workers().len();
418 let buffer_cache_strategy = config.dev_tweaks.buffer_cache_strategy();
419 let buffer_max_buckets = config.dev_tweaks.buffer_max_buckets;
420 let buffer_cache_allocation_strategy = config
421 .dev_tweaks
422 .effective_buffer_cache_allocation_strategy();
423 let fbuf_slab_bytes_per_class = config
424 .dev_tweaks
425 .fbuf_slab_bytes_per_class
426 .unwrap_or(FBufSlabs::DEFAULT_BYTES_PER_CLASS);
427 let storage = if let Some(storage) = config.storage.clone() {
428 let locked_directory =
429 LockedDirectory::new_blocking(storage.config.path(), Duration::from_secs(60))?;
430 let backend = storage.backend;
431
432 if let Some(init_checkpoint) = storage.init_checkpoint
433 && !backend
434 .exists(&Checkpointer::checkpoint_dir(init_checkpoint).child("CHECKPOINT"))?
435 {
436 return Err(DbspError::Storage(StorageError::CheckpointNotFound(
437 init_checkpoint,
438 )));
439 }
440
441 Some(RuntimeStorage {
442 config: storage.config,
443 options: storage.options,
444 backend,
445 locked_directory,
446 })
447 } else {
448 None
449 };
450
451 let total_cache_bytes = if let Some(storage) = &storage {
452 match storage.options.cache_mib {
453 Some(cache_mib) => cache_mib.saturating_mul(1024 * 1024),
454 None => 256usize
455 .saturating_mul(1024 * 1024)
456 .saturating_mul(nworkers)
457 .saturating_mul(ThreadType::LENGTH),
458 }
459 } else {
460 1
462 };
463
464 info!(
465 "Setting up buffer caches: {buffer_cache_strategy:?} {buffer_cache_allocation_strategy:?} buckets={buffer_max_buckets:?} total_size={:?} MiB",
466 total_cache_bytes / (1024 * 1024)
467 );
468 let buffer_caches = build_buffer_caches(
469 nworkers,
470 total_cache_bytes,
471 buffer_cache_strategy,
472 buffer_max_buckets,
473 buffer_cache_allocation_strategy,
474 );
475
476 info!("Setting up FBuf slab allocators: bytes_per_class={fbuf_slab_bytes_per_class}");
477 let fbuf_slab_allocators = (0..nworkers)
478 .map(|_| {
479 let allocator = Arc::new(FBufSlabs::new(fbuf_slab_bytes_per_class));
480 enum_map! {
481 ThreadType::Foreground => allocator.clone(),
482 ThreadType::Background => allocator.clone(),
483 }
484 })
485 .collect();
486
487 let (pin_cpus_fg, pin_cpus_bg) = map_pin_cpus(&config);
488
489 if !config.dev_tweaks.other_options.is_empty() {
490 warn!(
491 "Circuit dev_tweaks includes unknown options: {:?}",
492 &config.dev_tweaks.other_options
493 );
494 }
495
496 if config.dev_tweaks.streaming_exchange() {
497 info!("dev_tweaks.streaming_exchange enabled");
498 } else {
499 info!("dev_tweaks.streaming_exchange disabled");
500 }
501
502 Ok(Self {
503 pin_cpus_fg,
504 pin_cpus_bg,
505 layout: config.layout,
506 mode: config.mode,
507 step_size: config.step_size,
508 allow_input_during_commit: config.allow_input_during_commit,
509 dev_tweaks: config.dev_tweaks,
510 max_rss: config.max_rss_bytes,
511 process_rss: AtomicU64::new(process_rss_bytes().unwrap_or_default()),
512 memory_pressure: AtomicU8::new(MemoryPressure::Low as u8),
513 memory_pressure_epoch: AtomicU64::new(0),
514 memory_pressure_notify: Arc::new(Notify::new()),
515 storage,
516 store: TypedDashMap::new(),
517 kill_signal: AtomicBool::new(false),
518 cancellation_token: CancellationToken::new(),
519 aux_threads: Mutex::new(Vec::new()),
520 buffer_caches,
521 fbuf_slab_allocators,
522 worker_sequence_numbers: (0..nworkers).map(|_| AtomicUsize::new(0)).collect(),
523 panic_info: (0..nworkers)
524 .map(|_| EnumMap::from_fn(|_| RwLock::new(None)))
525 .collect(),
526 panicked: AtomicBool::new(false),
527 tokio_merger_runtime: Mutex::new(None),
528 exchange_listener: Mutex::new(config.exchange_listener),
529 })
530 }
531
532 fn pin_cpu(&self) {
533 match current_thread_type() {
534 Some(ThreadType::Foreground) => {
535 if !self.pin_cpus_fg.is_empty() {
536 let local_worker_offset = Runtime::local_worker_offset();
537 let core = self.pin_cpus_fg[local_worker_offset];
538 if !core_affinity::set_for_current(core) {
539 warn!(
540 "failed to pin foreground worker {local_worker_offset} to core {}",
541 core.id
542 );
543 }
544 }
545 }
546 Some(ThreadType::Background) => {
547 if !self.pin_cpus_bg.is_empty() {
548 let local_worker_offset = Runtime::local_worker_offset();
549 let core = self.pin_cpus_bg[local_worker_offset];
550 if !core_affinity::set_for_current(core) {
551 warn!(
552 "failed to pin background worker {local_worker_offset} to core {}",
553 core.id
554 );
555 }
556 }
557 }
558 None => {
559 panic!("pin_cpu() called outside of a runtime or on an aux thread");
560 }
561 }
562 }
563
564 fn memory_pressure(&self) -> MemoryPressure {
565 MemoryPressure::try_from(self.memory_pressure.load(Ordering::Relaxed)).unwrap()
566 }
567
568 fn raw_memory_pressure(&self, process_rss: u64) -> MemoryPressure {
572 let process_rss = process_rss as f64;
573
574 let Some(max_rss) = self.max_rss else {
575 return MemoryPressure::Low;
576 };
577
578 if process_rss >= (CRITICAL_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
579 return MemoryPressure::Critical;
580 }
581
582 if process_rss >= (HIGH_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
583 return MemoryPressure::High;
584 }
585
586 if process_rss >= (MODERATE_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
587 return MemoryPressure::Moderate;
588 }
589
590 MemoryPressure::Low
591 }
592}
593
594fn panic_hook(panic_info: &PanicHookInfo<'_>, default_panic_hook: &dyn Fn(&PanicHookInfo<'_>)) {
604 default_panic_hook(panic_info);
606
607 RUNTIME.with(|runtime| {
608 if let Ok(runtime) = runtime.try_borrow()
609 && let Some(runtime) = runtime.as_ref()
610 {
611 runtime.panic(panic_info);
612 }
613 })
614}
615
616#[repr(transparent)]
620#[derive(Clone, Debug)]
621pub struct Runtime(Arc<RuntimeInner>);
622
623#[repr(transparent)]
625#[derive(Clone, Debug)]
626pub struct WeakRuntime(Weak<RuntimeInner>);
627
628impl WeakRuntime {
629 pub fn upgrade(&self) -> Option<Runtime> {
630 self.0.upgrade().map(Runtime)
631 }
632}
633
634#[allow(clippy::type_complexity)]
637static DEFAULT_PANIC_HOOK: Lazy<Box<dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send>> =
638 Lazy::new(|| {
639 let _ = panic::take_hook();
641 panic::take_hook()
642 });
643
644fn default_panic_hook() -> &'static (dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send) {
646 &*DEFAULT_PANIC_HOOK
647}
648
649impl Runtime {
650 pub fn run<F>(config: impl Into<CircuitConfig>, circuit: F) -> Result<RuntimeHandle, DbspError>
689 where
690 F: FnOnce(Parker) + Clone + Send + 'static,
691 {
692 let mut config: CircuitConfig = config.into();
693 if config.step_size == StepSize::FullSteps {
694 config.dev_tweaks.splitter_chunk_size_records = Some(u64::MAX);
695 }
696
697 let workers = config.layout.local_workers();
698 let num_merger_threads = config.num_merger_threads();
699
700 let runtime = Self(Arc::new(RuntimeInner::new(config)?));
701
702 let default_hook = default_panic_hook();
704 panic::set_hook(Box::new(move |panic_info| {
705 panic_hook(panic_info, default_hook)
706 }));
707
708 let runtime_clone = runtime.clone();
710 let tokio_merger_runtime: TokioRuntime = {
711 info!("starting dbsp merger tokio runtime, workers: {num_merger_threads}",);
712
713 TokioBuilder::new_multi_thread()
714 .worker_threads(num_merger_threads)
715 .thread_name_fn(|| {
716 use std::sync::atomic::{AtomicUsize, Ordering};
717 static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
718 let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
719 format!("merger-{}", id)
720 })
721 .on_thread_start(move || {
722 set_current_thread_type(ThreadType::Background);
723 RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime_clone.clone()));
724 })
725 .thread_stack_size(6 * 1024 * 1024)
726 .enable_all()
727 .build()
728 .unwrap()
729 };
730
731 runtime
732 .inner()
733 .tokio_merger_runtime
734 .lock()
735 .unwrap()
736 .replace(tokio_merger_runtime);
737
738 runtime.spawn_aux_thread("rss-monitor", Parker::new(), |parker| {
740 let runtime = Runtime::runtime().unwrap();
741
742 let mut pressure_span = None;
743 while !Runtime::kill_in_progress() {
744 let process_rss = process_rss_bytes().unwrap_or_default();
745 runtime
746 .inner()
747 .process_rss
748 .store(process_rss, Ordering::Relaxed);
749
750 let previous_pressure = runtime.inner().memory_pressure();
751 let current_memory_pressure = runtime.inner().raw_memory_pressure(process_rss);
752
753 let new_memory_pressure = match (previous_pressure, current_memory_pressure) {
764 (MemoryPressure::Low | MemoryPressure::Moderate, _) => current_memory_pressure,
765 (MemoryPressure::High, MemoryPressure::Critical) => MemoryPressure::Critical,
766 (MemoryPressure::High, MemoryPressure::High | MemoryPressure::Moderate) => {
767 MemoryPressure::High
768 }
769 (MemoryPressure::High, MemoryPressure::Low) => MemoryPressure::Low,
770 (MemoryPressure::Critical, MemoryPressure::Critical | MemoryPressure::High) => {
771 MemoryPressure::Critical
772 }
773 (MemoryPressure::Critical, MemoryPressure::Moderate | MemoryPressure::Low) => {
774 current_memory_pressure
775 }
776 };
777 if pressure_span.is_none() || new_memory_pressure != previous_pressure {
778 pressure_span = Some(
779 LongSpanBuilder::new("memory-pressure")
780 .with_tooltip(new_memory_pressure.to_string())
781 .build(),
782 );
783 }
784
785 runtime
786 .inner()
787 .memory_pressure
788 .store(new_memory_pressure as u8, Ordering::Relaxed);
789
790 if previous_pressure < MemoryPressure::High
792 && new_memory_pressure >= MemoryPressure::High
793 {
794 runtime
795 .inner()
796 .memory_pressure_epoch
797 .fetch_add(1, Ordering::Relaxed);
798 runtime.inner().memory_pressure_notify.notify_waiters();
799 }
800 parker.park_timeout(Duration::from_secs(1));
801 }
802 drop(pressure_span);
803 });
804
805 let workers = workers
806 .map(|worker_index| {
807 let runtime = runtime.clone();
808 let build_circuit = circuit.clone();
809 let parker = Parker::new();
810 let unparker = parker.unparker().clone();
811 let local_worker_offset =
812 worker_index - runtime.inner().layout.local_workers().start;
813 let fbuf_slab_allocator =
814 runtime.get_fbuf_slab_allocator(local_worker_offset, ThreadType::Foreground);
815 let handle = Builder::new()
816 .name(format!("dbsp-worker-{worker_index}"))
817 .spawn(move || {
818 WORKER_INDEX.set(worker_index);
820 set_current_thread_type(ThreadType::Foreground);
821 set_thread_slab_pool(Some(fbuf_slab_allocator));
822 runtime.inner().pin_cpu();
823 RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
824
825 build_circuit(parker);
827 })
828 .unwrap_or_else(|error| {
829 panic!("failed to spawn worker thread {worker_index}: {error}");
830 });
831 (handle, unparker)
832 })
833 .collect::<Vec<_>>();
834
835 Ok(RuntimeHandle::new(runtime, workers))
836 }
837
838 pub fn downgrade(&self) -> WeakRuntime {
839 WeakRuntime(Arc::downgrade(&self.0))
840 }
841
842 #[allow(clippy::self_named_constructors)]
852 pub fn runtime() -> Option<Runtime> {
853 RUNTIME.with(|rt| rt.borrow().clone())
854 }
855
856 pub fn storage_backend() -> Result<Arc<dyn StorageBackend>, StorageError> {
862 Runtime::runtime()
863 .unwrap()
864 .inner()
865 .storage
866 .as_ref()
867 .map_or(Err(StorageError::StorageDisabled), |storage| {
868 Ok(storage.backend.clone())
869 })
870 }
871
872 pub fn buffer_cache() -> Option<Arc<BufferCache>> {
891 if let Some(buffer_cache) = BUFFER_CACHE.with(|bc| bc.borrow().clone()) {
892 Some(buffer_cache)
894 } else if let Ok(buffer_cache) = TOKIO_BUFFER_CACHE.try_get() {
895 Some(buffer_cache)
897 } else if let Some(rt) = Runtime::runtime()
898 && let Some(thread_type) = current_thread_type()
899 {
900 match thread_type {
902 ThreadType::Foreground => {
903 let buffer_cache =
904 rt.get_buffer_cache(Runtime::local_worker_offset(), thread_type);
905 BUFFER_CACHE.set(Some(buffer_cache.clone()));
906 Some(buffer_cache)
907 }
908 ThreadType::Background => {
909 None
915 }
916 }
917 } else {
918 static AUXILIARY_CACHE: LazyLock<Arc<BufferCache>> =
931 LazyLock::new(|| Arc::new(BufferCache::new(1024 * 1024 * 256)));
932
933 let buffer_cache = AUXILIARY_CACHE.clone();
934 BUFFER_CACHE.set(Some(buffer_cache.clone()));
935 Some(buffer_cache)
936 }
937 }
938
939 pub fn spawn_aux_thread<F>(&self, thread_name: &str, parker: Parker, f: F)
952 where
953 F: FnOnce(Parker) + Send + 'static,
954 {
955 let runtime = self.clone();
956 let unparker = parker.unparker().clone();
957 let handle = Builder::new()
958 .name(thread_name.to_string())
959 .spawn(move || {
960 RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
961 f(parker)
962 })
963 .expect("failed to spawn auxiliary thread");
964
965 self.inner()
966 .aux_threads
967 .lock()
968 .unwrap()
969 .push((handle, unparker))
970 }
971
972 pub fn get_buffer_cache(
977 &self,
978 local_worker_offset: usize,
979 thread_type: ThreadType,
980 ) -> Arc<BufferCache> {
981 self.0.buffer_caches[local_worker_offset][thread_type].clone()
982 }
983
984 pub(crate) fn get_fbuf_slab_allocator(
985 &self,
986 local_worker_offset: usize,
987 thread_type: ThreadType,
988 ) -> Arc<FBufSlabs> {
989 self.0.fbuf_slab_allocators[local_worker_offset][thread_type].clone()
990 }
991
992 pub fn cache_occupancy(&self) -> (usize, usize) {
995 if self.0.storage.is_some() {
996 let mut seen = HashSet::new();
997 self.0
998 .buffer_caches
999 .iter()
1000 .flat_map(|map| map.values())
1001 .filter(|cache| seen.insert(cache.backend_id()))
1002 .map(|cache| cache.occupancy())
1003 .fold((0, 0), |(a_cur, a_max), (b_cur, b_max)| {
1004 (a_cur + b_cur, a_max + b_max)
1005 })
1006 } else {
1007 (0, 0)
1008 }
1009 }
1010
1011 pub fn fbuf_slabs_stats(&self) -> FBufSlabsStats {
1013 if self.0.storage.is_some() {
1014 let mut seen = HashSet::new();
1015 self.0
1016 .fbuf_slab_allocators
1017 .iter()
1018 .flat_map(|map| map.values())
1019 .filter(|allocator| seen.insert(allocator.backend_id()))
1020 .map(|allocator| allocator.stats())
1021 .fold(FBufSlabsStats::default(), |mut stats, pool_stats| {
1022 stats += pool_stats;
1023 stats
1024 })
1025 } else {
1026 FBufSlabsStats::default()
1027 }
1028 }
1029
1030 pub fn worker_index() -> usize {
1037 match current_thread_type() {
1038 Some(ThreadType::Foreground) => WORKER_INDEX.get(),
1039 Some(ThreadType::Background) => TOKIO_WORKER_INDEX.get(),
1040 None => RUNTIME.with_borrow(|runtime| match runtime {
1041 Some(runtime) => {
1042 runtime.layout().local_workers().start
1045 }
1046 None => {
1047 0
1049 }
1050 }),
1051 }
1052 }
1053
1054 pub fn local_worker_offset() -> usize {
1056 let local_workers_start = RUNTIME
1058 .with(|rt| Some(rt.borrow().as_ref()?.layout().local_workers().start))
1059 .unwrap_or_default();
1060 Self::worker_index() - local_workers_start
1061 }
1062
1063 pub fn mode() -> Mode {
1064 RUNTIME
1065 .with(|rt| Some(rt.borrow().as_ref()?.get_mode()))
1066 .unwrap_or_default()
1067 }
1068
1069 pub fn with_dev_tweaks<F, T>(f: F) -> T
1070 where
1071 F: Fn(&DevTweaks) -> T,
1072 {
1073 static DEFAULT: Lazy<DevTweaks> = Lazy::new(DevTweaks::default);
1074 RUNTIME
1075 .with(|rt| Some(f(&rt.borrow().as_ref()?.inner().dev_tweaks)))
1076 .unwrap_or_else(|| f(&DEFAULT))
1077 }
1078
1079 pub fn get_mode(&self) -> Mode {
1080 self.inner().mode
1081 }
1082
1083 pub fn step_size(&self) -> StepSize {
1084 self.inner().step_size
1085 }
1086
1087 pub fn allow_input_during_commit(&self) -> bool {
1088 self.inner().allow_input_during_commit
1089 }
1090
1091 pub fn worker_index_str() -> &'static str {
1095 static WORKER_INDEX_STRS: Lazy<[&'static str; 256]> = Lazy::new(|| {
1096 let mut data: [&'static str; 256] = [""; 256];
1097 for (i, item) in data.iter_mut().enumerate() {
1098 *item = Box::leak(i.to_string().into_boxed_str());
1099 }
1100 data
1101 });
1102
1103 WORKER_INDEX_STRS
1104 .get(Runtime::worker_index())
1105 .copied()
1106 .unwrap_or_else(|| {
1107 panic!("Limit workers to less than 256 or increase the limit in the code.")
1108 })
1109 }
1110
1111 pub fn memory_pressure() -> Option<MemoryPressure> {
1112 RUNTIME.with(|rt| Some(rt.borrow().as_ref()?.inner().memory_pressure()))
1113 }
1114
1115 pub fn current_memory_pressure(&self) -> MemoryPressure {
1116 self.inner().memory_pressure()
1117 }
1118
1119 pub fn max_rss_bytes(&self) -> Option<u64> {
1120 self.inner().max_rss
1121 }
1122
1123 pub fn memory_pressure_epoch(&self) -> u64 {
1124 self.inner().memory_pressure_epoch.load(Ordering::Relaxed)
1125 }
1126
1127 pub(crate) fn memory_pressure_notify(&self) -> Arc<Notify> {
1128 self.inner().memory_pressure_notify.clone()
1129 }
1130
1131 pub fn min_merge_storage_bytes() -> Option<usize> {
1143 RUNTIME.with(|rt| {
1144 let rt = rt.borrow();
1145 let inner = rt.as_ref()?.inner();
1146 let storage = inner.storage.as_ref()?;
1147
1148 if inner.memory_pressure() >= MemoryPressure::Moderate {
1149 Some(0)
1150 } else {
1151 Some(storage.options.min_storage_bytes.unwrap_or({
1152 10 * 1024 * 1024
1155 }))
1156 }
1157 })
1158 }
1159
1160 pub fn min_insert_storage_bytes() -> Option<usize> {
1172 RUNTIME.with(|rt| {
1173 let rt = rt.borrow();
1174 let inner = rt.as_ref()?.inner();
1175 let storage = inner.storage.as_ref()?;
1176
1177 if inner.memory_pressure() >= MemoryPressure::High {
1178 Some(0)
1179 } else if inner.memory_pressure() >= MemoryPressure::Moderate {
1180 Some(
1182 storage
1183 .options
1184 .min_storage_bytes
1185 .unwrap_or(10 * 1024 * 1024),
1186 )
1187 } else {
1188 Some(usize::MAX)
1191 }
1192 })
1193 }
1194
1195 pub fn min_step_storage_bytes() -> Option<usize> {
1208 RUNTIME.with(|rt| {
1209 let rt = rt.borrow();
1210 let inner = rt.as_ref()?.inner();
1211 let storage = inner.storage.as_ref()?;
1212
1213 if inner.memory_pressure() >= MemoryPressure::Critical {
1214 Some(0)
1215 } else {
1216 Some(storage.options.min_step_storage_bytes.unwrap_or(usize::MAX))
1217 }
1218 })
1219 }
1220
1221 pub fn file_writer_parameters() -> Parameters {
1222 let compression = Runtime::runtime()
1223 .unwrap()
1224 .inner()
1225 .storage
1226 .as_ref()
1227 .unwrap()
1228 .options
1229 .compression;
1230 let compression = match compression {
1231 StorageCompression::Default | StorageCompression::Snappy => Some(Compression::Snappy),
1232 StorageCompression::None => None,
1233 };
1234 Parameters::default().with_compression(compression)
1235 }
1236
1237 fn inner(&self) -> &RuntimeInner {
1238 &self.0
1239 }
1240
1241 pub fn num_workers() -> usize {
1246 RUNTIME.with(|rt| {
1247 rt.borrow()
1248 .as_ref()
1249 .map_or(1, |runtime| runtime.layout().n_workers())
1250 })
1251 }
1252
1253 pub fn layout(&self) -> &Layout {
1255 &self.inner().layout
1256 }
1257
1258 pub fn local_store(&self) -> &LocalStore {
1269 &self.inner().store
1270 }
1271
1272 pub fn storage_path(&self) -> Option<&Path> {
1274 self.inner()
1275 .storage
1276 .as_ref()
1277 .map(|storage| storage.config.path())
1278 }
1279
1280 pub fn sequence_next(&self) -> usize {
1286 self.inner().worker_sequence_numbers[Self::local_worker_offset()]
1287 .fetch_add(1, Ordering::Relaxed)
1288 }
1289
1290 pub fn kill_in_progress() -> bool {
1294 RUNTIME.with(|runtime| {
1298 runtime
1299 .borrow()
1300 .as_ref()
1301 .map(|runtime| runtime.inner().kill_signal.load(Ordering::SeqCst))
1302 .unwrap_or(false)
1303 })
1304 }
1305
1306 pub fn cancellation_token(&self) -> CancellationToken {
1307 self.inner().cancellation_token.clone()
1308 }
1309
1310 pub fn worker_panic_info(
1311 &self,
1312 worker: usize,
1313 thread_type: ThreadType,
1314 ) -> Option<WorkerPanicInfo> {
1315 if let Ok(guard) = self.inner().panic_info[worker][thread_type].read() {
1316 guard.clone()
1317 } else {
1318 warn!("poisoned panic_lock lock for {thread_type} worker {worker}");
1319 None
1320 }
1321 }
1322
1323 fn panic(&self, panic_info: &PanicHookInfo) {
1325 let local_worker_offset = Self::local_worker_offset();
1326 let Some(thread_type) = current_thread_type() else {
1327 error!("panic hook called outside of a runtime or on an aux thread");
1330 return;
1331 };
1332 let panic_info = WorkerPanicInfo::new(panic_info);
1333 let _ = self.inner().panic_info[local_worker_offset][thread_type]
1334 .write()
1335 .map(|mut guard| *guard = Some(panic_info));
1336 self.inner().panicked.store(true, Ordering::Release);
1337 }
1338
1339 pub(crate) fn tokio_merger_runtime(&self) -> Option<tokio::runtime::Handle> {
1346 self.inner()
1347 .tokio_merger_runtime
1348 .lock()
1349 .unwrap()
1350 .as_ref()
1351 .map(|rt| rt.handle().clone())
1352 }
1353
1354 pub(crate) fn take_exchange_listener(&self) -> Option<TcpListener> {
1359 self.inner().exchange_listener.lock().unwrap().take()
1360 }
1361
1362 pub fn dev_tweaks(&self) -> &DevTweaks {
1366 &self.inner().dev_tweaks
1367 }
1368}
1369
1370pub(crate) struct Consensus(Broadcast<bool>);
1373
1374impl Consensus {
1375 pub fn new(name: impl Display) -> Self {
1376 Self(Broadcast::new(name))
1377 }
1378
1379 pub async fn check(&self, local: bool) -> Result<bool, SchedulerError> {
1385 Ok(self.0.collect(local).await?.into_iter().all(identity))
1386 }
1387}
1388
1389pub(crate) enum Broadcast<T> {
1392 SingleThreaded,
1393 MultiThreaded {
1394 exchange: Arc<Exchange<T>>,
1395 identifier: Arc<String>,
1396 },
1397}
1398
1399impl<T> Broadcast<T>
1400where
1401 T: Clone + Debug + Send + serde::Serialize + for<'de> serde::Deserialize<'de> + 'static,
1402{
1403 pub fn new(name: impl Display) -> Self {
1404 match Runtime::runtime() {
1405 Some(runtime) if Runtime::num_workers() > 1 => {
1406 let exchange_id = runtime.sequence_next().try_into().unwrap();
1407 let exchange =
1408 Exchange::with_runtime(&runtime, exchange_id, ExchangeActivity::AllSteps);
1409 let identifier = Arc::new(format!("broadcast {name} (exchange {exchange_id})"));
1410
1411 Self::MultiThreaded {
1412 exchange,
1413 identifier,
1414 }
1415 }
1416 _ => Self::SingleThreaded,
1417 }
1418 }
1419
1420 pub async fn collect(&self, local: T) -> Result<Vec<T>, SchedulerError> {
1426 match self {
1427 Self::SingleThreaded => Ok(vec![local]),
1428 Self::MultiThreaded {
1429 exchange,
1430 identifier,
1431 } => {
1432 let _span = Span::new("broadcast")
1435 .with_category("Exchange")
1436 .with_tooltip(|| format!("collect for {}", identifier));
1437
1438 Runtime::runtime()
1439 .unwrap()
1440 .cancellation_token()
1441 .run_until_cancelled_owned(async {
1442 exchange
1443 .send_all_with_serializer(identifier, repeat(local.clone()), |local| {
1444 let mut fbuf = FBuf::new();
1445 serde_json::to_writer(&mut fbuf, &local).unwrap();
1446 fbuf
1447 })
1448 .await;
1449
1450 exchange
1451 .receive_all(|data| serde_json::from_slice(&data).unwrap(), None)
1452 .await
1453 .0
1454 })
1455 .await
1456 .ok_or(SchedulerError::Killed)
1457 }
1458 }
1459 }
1460}
1461
1462#[derive(Debug)]
1464pub struct RuntimeHandle {
1465 runtime: Runtime,
1466 workers: Vec<(JoinHandle<()>, Unparker)>,
1467}
1468
1469impl RuntimeHandle {
1470 fn new(runtime: Runtime, workers: Vec<(JoinHandle<()>, Unparker)>) -> Self {
1471 Self { runtime, workers }
1472 }
1473
1474 pub(super) fn unpark_worker(&self, worker: usize) {
1480 self.workers[worker].1.unpark();
1481 }
1482
1483 pub fn runtime(&self) -> &Runtime {
1485 &self.runtime
1486 }
1487
1488 pub fn kill(self) -> ThreadResult<()> {
1496 self.kill_async();
1497 self.join()
1498 }
1499
1500 pub fn kill_async(&self) {
1503 self.runtime
1504 .inner()
1505 .kill_signal
1506 .store(true, Ordering::SeqCst);
1507 self.runtime.inner().cancellation_token.cancel();
1508 for (_worker, unparker) in self.workers.iter() {
1509 unparker.unpark();
1510 }
1511
1512 self.runtime
1513 .inner()
1514 .aux_threads
1515 .lock()
1516 .unwrap()
1517 .iter()
1518 .for_each(|(_h, unparker)| {
1519 unparker.unpark();
1520 });
1521 }
1522
1523 pub fn join(self) -> ThreadResult<()> {
1527 #[allow(clippy::needless_collect)]
1529 let results: Vec<ThreadResult<()>> = self
1530 .workers
1531 .into_iter()
1532 .map(|(h, _unparker)| h.join())
1533 .collect();
1534
1535 self.runtime
1540 .inner()
1541 .kill_signal
1542 .store(true, Ordering::SeqCst);
1543
1544 self.runtime
1545 .inner()
1546 .aux_threads
1547 .lock()
1548 .unwrap()
1549 .iter()
1550 .for_each(|(_h, unparker)| {
1551 unparker.unpark();
1552 });
1553
1554 self.runtime
1556 .inner()
1557 .aux_threads
1558 .lock()
1559 .unwrap()
1560 .drain(..)
1561 .for_each(|(h, _unparker)| {
1562 let _ = h.join();
1563 });
1564
1565 let tokio_merger_runtime = self
1575 .runtime
1576 .inner()
1577 .tokio_merger_runtime
1578 .lock()
1579 .unwrap()
1580 .take();
1581 drop(tokio_merger_runtime);
1582
1583 self.runtime.local_store().clear();
1588
1589 results.into_iter().collect()
1590 }
1591
1592 pub fn worker_panic_info(
1594 &self,
1595 worker: usize,
1596 thread_type: ThreadType,
1597 ) -> Option<WorkerPanicInfo> {
1598 self.runtime.worker_panic_info(worker, thread_type)
1599 }
1600
1601 pub fn collect_panic_info(&self) -> Vec<(usize, ThreadType, WorkerPanicInfo)> {
1603 let mut result = Vec::new();
1604
1605 for worker in 0..self.workers.len() {
1606 for thread_type in [ThreadType::Foreground, ThreadType::Background] {
1607 if let Some(panic_info) = self.worker_panic_info(worker, thread_type) {
1608 result.push((worker, thread_type, panic_info))
1609 }
1610 }
1611 }
1612 result
1613 }
1614
1615 pub fn panicked(&self) -> bool {
1617 self.runtime.inner().panicked.load(Ordering::Acquire)
1618 }
1619}
1620
1621#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1623pub enum WorkerLocation {
1624 Local,
1626 Remote,
1628}
1629
1630#[derive(Clone, Debug)]
1634pub struct WorkerLocations {
1635 workers: Range<usize>,
1636 local_workers: Range<usize>,
1637}
1638
1639impl Default for WorkerLocations {
1640 fn default() -> Self {
1641 Self::new()
1642 }
1643}
1644
1645impl WorkerLocations {
1646 pub fn new() -> Self {
1650 if let Some(runtime) = Runtime::runtime() {
1651 Self::for_layout(runtime.layout())
1652 } else {
1653 Self {
1654 workers: 0..1,
1655 local_workers: 0..1,
1656 }
1657 }
1658 }
1659
1660 pub fn for_layout(layout: &Layout) -> Self {
1662 Self {
1663 workers: 0..layout.n_workers(),
1664 local_workers: layout.local_workers(),
1665 }
1666 }
1667
1668 pub fn get(&self, worker: usize) -> WorkerLocation {
1670 self[worker]
1671 }
1672}
1673
1674impl Index<usize> for WorkerLocations {
1675 type Output = WorkerLocation;
1676
1677 fn index(&self, worker: usize) -> &Self::Output {
1678 debug_assert!(worker < self.workers.end);
1679 if self.local_workers.contains(&worker) {
1680 &WorkerLocation::Local
1681 } else {
1682 &WorkerLocation::Remote
1683 }
1684 }
1685}
1686
1687impl Iterator for WorkerLocations {
1688 type Item = WorkerLocation;
1689
1690 fn next(&mut self) -> Option<Self::Item> {
1691 let worker = self.workers.next()?;
1692 Some(self.get(worker))
1693 }
1694
1695 fn size_hint(&self) -> (usize, Option<usize>) {
1696 let len = self.workers.len();
1697 (len, Some(len))
1698 }
1699}
1700
1701impl ExactSizeIterator for WorkerLocations {}
1702
1703#[cfg(test)]
1704mod tests {
1705 use super::{Runtime, RuntimeInner};
1706 use crate::{
1707 Circuit, RootCircuit,
1708 circuit::{
1709 CircuitConfig,
1710 dbsp_handle::CircuitStorageConfig,
1711 metadata::{LOOSE_MEMORY_RECORDS_COUNT, MERGING_MEMORY_RECORDS_COUNT},
1712 schedule::{DynamicScheduler, Scheduler},
1713 },
1714 operator::Generator,
1715 profile::DbspProfile,
1716 storage::backend::FileId,
1717 };
1718 use enum_map::Enum;
1719 use feldera_buffer_cache::{CacheEntry, ThreadType};
1720 use feldera_storage::fbuf::{FBuf, slab::set_thread_slab_pool};
1721 use feldera_types::config::{
1722 StorageCacheConfig, StorageConfig, StorageOptions,
1723 dev_tweaks::{BufferCacheAllocationStrategy, BufferCacheStrategy},
1724 };
1725 use feldera_types::memory_pressure::MemoryPressure;
1726 use std::{
1727 cell::RefCell,
1728 rc::Rc,
1729 sync::{Arc, LazyLock, Mutex},
1730 thread::sleep,
1731 time::{Duration, Instant},
1732 };
1733
1734 struct TestCacheEntry(usize);
1735
1736 impl CacheEntry for TestCacheEntry {
1737 fn cost(&self) -> usize {
1738 self.0
1739 }
1740 }
1741
1742 static MOCK_RSS_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1744
1745 fn set_mock_process_rss_bytes(bytes: u64) {
1746 unsafe {
1748 std::env::set_var("MOCK_PROCESS_RSS_BYTES", bytes.to_string());
1749 }
1750 }
1751
1752 struct MockRssVarGuard;
1753
1754 impl Drop for MockRssVarGuard {
1755 fn drop(&mut self) {
1756 unsafe {
1758 std::env::remove_var("MOCK_PROCESS_RSS_BYTES");
1759 }
1760 }
1761 }
1762
1763 fn query_runtime_memory_state(runtime: &Runtime) -> (MemoryPressure, usize, usize, usize) {
1764 let (sender, receiver) = std::sync::mpsc::channel();
1765 runtime.spawn_aux_thread(
1766 "memory-pressure-test-query",
1767 super::Parker::new(),
1768 move |_parker| {
1769 let _ = sender.send((
1770 Runtime::memory_pressure().expect("query thread should run inside runtime"),
1771 Runtime::min_merge_storage_bytes().expect("runtime has storage configured"),
1772 Runtime::min_insert_storage_bytes().expect("runtime has storage configured"),
1773 Runtime::min_step_storage_bytes().expect("runtime has storage configured"),
1774 ));
1775 },
1776 );
1777 receiver
1778 .recv_timeout(Duration::from_secs(5))
1779 .expect("failed to query memory state from runtime thread")
1780 }
1781
1782 fn count_metric(
1783 profile: &DbspProfile,
1784 metric: &'static crate::circuit::metadata::MetricId,
1785 ) -> usize {
1786 let root_node = crate::circuit::GlobalNodeId::root();
1787 profile
1788 .worker_profiles
1789 .iter()
1790 .map(|worker| {
1791 worker
1792 .get_node_profile(&root_node)
1793 .map(|meta| {
1794 meta.iter()
1795 .filter_map(|((metric_id, _labels), value)| {
1796 if metric_id == metric {
1797 match value {
1798 crate::circuit::metadata::MetaItem::Count(count) => {
1799 Some(*count)
1800 }
1801 _ => None,
1802 }
1803 } else {
1804 None
1805 }
1806 })
1807 .sum::<usize>()
1808 })
1809 .unwrap_or(0)
1810 })
1811 .sum()
1812 }
1813
1814 fn in_memory_spine_records(profile: &DbspProfile) -> usize {
1815 count_metric(profile, &LOOSE_MEMORY_RECORDS_COUNT)
1816 + count_metric(profile, &MERGING_MEMORY_RECORDS_COUNT)
1817 }
1818
1819 #[test]
1820 #[cfg_attr(miri, ignore)]
1821 fn test_runtime_dynamic() {
1822 test_runtime::<DynamicScheduler>();
1823 }
1824
1825 #[test]
1826 #[cfg_attr(miri, ignore)]
1827 fn storage_no_cleanup() {
1828 let path = tempfile::tempdir().unwrap().keep();
1830 let path_clone = path.clone();
1831 let cconf = CircuitConfig::with_workers(4).with_storage(Some(
1832 CircuitStorageConfig::for_config(
1833 StorageConfig {
1834 path: path.to_string_lossy().into_owned(),
1835 cache: StorageCacheConfig::default(),
1836 },
1837 StorageOptions::default(),
1838 )
1839 .unwrap(),
1840 ));
1841
1842 let hruntime = Runtime::run(cconf, move |_parker| {
1843 let runtime = Runtime::runtime().unwrap();
1844 assert_eq!(runtime.storage_path(), Some(path_clone.as_ref()));
1845 })
1846 .expect("failed to start runtime");
1847 hruntime.join().unwrap();
1848 assert!(path.exists(), "persistent storage is not cleaned up");
1849 }
1850
1851 #[test]
1852 fn s3_fifo_is_the_default_buffer_cache_strategy() {
1853 let inner = RuntimeInner::new(CircuitConfig::with_workers(1)).unwrap();
1854 assert_eq!(
1855 inner.buffer_caches[0][ThreadType::Foreground].strategy(),
1856 BufferCacheStrategy::S3Fifo
1857 );
1858 }
1859
1860 #[test]
1861 fn default_s3_fifo_cache_shares_backend_per_worker_pair() {
1862 let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
1863 assert!(
1864 inner.buffer_caches[0][ThreadType::Foreground]
1865 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1866 );
1867 assert!(
1868 inner.buffer_caches[1][ThreadType::Foreground]
1869 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1870 );
1871 assert!(
1872 !inner.buffer_caches[0][ThreadType::Foreground]
1873 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground])
1874 );
1875 }
1876
1877 #[test]
1878 fn lru_can_still_be_selected_explicitly() {
1879 let inner = RuntimeInner::new(
1880 CircuitConfig::with_workers(1).with_buffer_cache_strategy(BufferCacheStrategy::Lru),
1881 )
1882 .unwrap();
1883 assert_eq!(
1884 inner.buffer_caches[0][ThreadType::Foreground].strategy(),
1885 BufferCacheStrategy::Lru
1886 );
1887 assert!(
1888 !inner.buffer_caches[0][ThreadType::Foreground]
1889 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1890 );
1891 }
1892
1893 #[test]
1894 fn lru_uses_default_total_cache_capacity_when_cache_mib_is_unset() {
1895 let workers = 3usize;
1896 let path = tempfile::tempdir().unwrap();
1897 let storage = CircuitStorageConfig::for_config(
1898 StorageConfig {
1899 path: path.path().to_string_lossy().into_owned(),
1900 cache: StorageCacheConfig::default(),
1901 },
1902 StorageOptions::default(),
1903 )
1904 .unwrap();
1905 let runtime = Runtime(Arc::new(
1906 RuntimeInner::new(
1907 CircuitConfig::with_workers(workers)
1908 .with_storage(Some(storage))
1909 .with_buffer_cache_strategy(BufferCacheStrategy::Lru),
1910 )
1911 .unwrap(),
1912 ));
1913
1914 assert_eq!(
1915 runtime.cache_occupancy(),
1916 (0, workers * ThreadType::LENGTH * 256usize * 1024 * 1024)
1917 );
1918 }
1919
1920 #[test]
1921 fn s3_fifo_cache_can_share_cache_per_worker_pair_when_requested() {
1922 let config = CircuitConfig::with_workers(2).with_buffer_cache_allocation_strategy(
1923 BufferCacheAllocationStrategy::SharedPerWorkerPair,
1924 );
1925 let inner = RuntimeInner::new(config).unwrap();
1926 assert!(
1927 inner.buffer_caches[0][ThreadType::Foreground]
1928 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1929 );
1930 assert!(
1931 inner.buffer_caches[1][ThreadType::Foreground]
1932 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1933 );
1934 }
1935
1936 #[test]
1937 fn fbuf_slabs_are_shared_per_worker_pair() {
1938 let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
1939 assert!(Arc::ptr_eq(
1940 &inner.fbuf_slab_allocators[0][ThreadType::Foreground],
1941 &inner.fbuf_slab_allocators[0][ThreadType::Background],
1942 ));
1943 assert!(Arc::ptr_eq(
1944 &inner.fbuf_slab_allocators[1][ThreadType::Foreground],
1945 &inner.fbuf_slab_allocators[1][ThreadType::Background],
1946 ));
1947 assert!(!Arc::ptr_eq(
1948 &inner.fbuf_slab_allocators[0][ThreadType::Foreground],
1949 &inner.fbuf_slab_allocators[1][ThreadType::Foreground],
1950 ));
1951 }
1952
1953 #[test]
1954 fn fbuf_slabs_honor_bytes_per_class_dev_tweak() {
1955 let inner =
1956 RuntimeInner::new(CircuitConfig::with_workers(1).with_fbuf_slab_bytes_per_class(4096))
1957 .unwrap();
1958 let allocator = inner.fbuf_slab_allocators[0][ThreadType::Foreground].clone();
1959 let previous = set_thread_slab_pool(Some(allocator.clone()));
1960
1961 let first = FBuf::with_capacity(4096);
1962 let second = FBuf::with_capacity(4096);
1963 drop(first);
1964 drop(second);
1965
1966 set_thread_slab_pool(previous);
1967
1968 assert_eq!(allocator.stats().cached_buffers, 1);
1969 }
1970
1971 #[test]
1972 fn sieve_can_share_one_global_cache_when_requested() {
1973 let config = CircuitConfig::with_workers(2)
1974 .with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global);
1975 let inner = RuntimeInner::new(config).unwrap();
1976 let global = inner.buffer_caches[0][ThreadType::Foreground].clone();
1977 assert!(global.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background]));
1978 assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground]));
1979 assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background]));
1980 }
1981
1982 #[test]
1983 fn lru_keeps_separate_foreground_and_background_caches() {
1984 let config = CircuitConfig::with_workers(2)
1985 .with_buffer_cache_strategy(BufferCacheStrategy::Lru)
1986 .with_buffer_cache_allocation_strategy(
1987 BufferCacheAllocationStrategy::SharedPerWorkerPair,
1988 );
1989 let inner = RuntimeInner::new(config).unwrap();
1990 assert!(
1991 !inner.buffer_caches[0][ThreadType::Foreground]
1992 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1993 );
1994 assert!(
1995 !inner.buffer_caches[1][ThreadType::Foreground]
1996 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1997 );
1998 }
1999
2000 #[test]
2001 fn shared_sharded_cache_occupancy_is_not_double_counted() {
2002 let path = tempfile::tempdir().unwrap();
2003 let storage = CircuitStorageConfig::for_config(
2004 StorageConfig {
2005 path: path.path().to_string_lossy().into_owned(),
2006 cache: StorageCacheConfig::default(),
2007 },
2008 StorageOptions {
2009 cache_mib: Some(8),
2010 ..StorageOptions::default()
2011 },
2012 )
2013 .unwrap();
2014 let runtime = Runtime(Arc::new(
2015 RuntimeInner::new(
2016 CircuitConfig::with_workers(1)
2017 .with_storage(Some(storage))
2018 .with_buffer_cache_allocation_strategy(
2019 BufferCacheAllocationStrategy::SharedPerWorkerPair,
2020 ),
2021 )
2022 .unwrap(),
2023 ));
2024
2025 runtime.get_buffer_cache(0, ThreadType::Foreground).insert(
2026 FileId::new(),
2027 0,
2028 Arc::new(TestCacheEntry(1024)),
2029 );
2030
2031 assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
2032 }
2033
2034 #[test]
2035 fn global_sharded_cache_occupancy_is_not_double_counted() {
2036 let path = tempfile::tempdir().unwrap();
2037 let storage = CircuitStorageConfig::for_config(
2038 StorageConfig {
2039 path: path.path().to_string_lossy().into_owned(),
2040 cache: StorageCacheConfig::default(),
2041 },
2042 StorageOptions {
2043 cache_mib: Some(8),
2044 ..StorageOptions::default()
2045 },
2046 )
2047 .unwrap();
2048 let runtime = Runtime(Arc::new(
2049 RuntimeInner::new(
2050 CircuitConfig::with_workers(2)
2051 .with_storage(Some(storage))
2052 .with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global),
2053 )
2054 .unwrap(),
2055 ));
2056
2057 runtime.get_buffer_cache(1, ThreadType::Background).insert(
2058 FileId::new(),
2059 0,
2060 Arc::new(TestCacheEntry(1024)),
2061 );
2062
2063 assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
2064 }
2065
2066 #[test]
2067 fn shared_fbuf_slab_stats_are_not_double_counted() {
2068 let path = tempfile::tempdir().unwrap();
2069 let storage = CircuitStorageConfig::for_config(
2070 StorageConfig {
2071 path: path.path().to_string_lossy().into_owned(),
2072 cache: StorageCacheConfig::default(),
2073 },
2074 StorageOptions::default(),
2075 )
2076 .unwrap();
2077 let runtime = Runtime(Arc::new(
2078 RuntimeInner::new(CircuitConfig::with_workers(1).with_storage(Some(storage))).unwrap(),
2079 ));
2080
2081 let previous = set_thread_slab_pool(Some(
2082 runtime.get_fbuf_slab_allocator(0, ThreadType::Foreground),
2083 ));
2084
2085 let first = FBuf::with_capacity(4096);
2086 drop(first);
2087 let second = FBuf::with_capacity(3000);
2088 drop(second);
2089
2090 set_thread_slab_pool(previous);
2091
2092 let stats = runtime.fbuf_slabs_stats();
2093 let class = stats
2094 .classes
2095 .iter()
2096 .find(|class| class.size == 4096)
2097 .unwrap();
2098
2099 assert_eq!(stats.alloc_requests(), 2);
2100 assert_eq!(stats.mallocs_saved(), 1);
2101 assert_eq!(stats.frees_saved(), 2);
2102 assert_eq!(stats.cached_buffers, 1);
2103 assert_eq!(class.alloc_requests, 2);
2104 assert_eq!(class.alloc_hits, 1);
2105 assert_eq!(class.recycle_requests, 2);
2106 assert_eq!(class.recycle_hits, 2);
2107 }
2108
2109 fn test_runtime<S>()
2110 where
2111 S: Scheduler + 'static,
2112 {
2113 let hruntime = Runtime::run(4, |_parker| {
2114 let data = Rc::new(RefCell::new(vec![]));
2115 let data_clone = data.clone();
2116 let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
2117 let runtime = Runtime::runtime().unwrap();
2118 circuit
2120 .add_source(Generator::new(move || runtime.sequence_next()))
2121 .inspect(move |n: &usize| data_clone.borrow_mut().push(*n));
2122 Ok(())
2123 })
2124 .unwrap()
2125 .0;
2126
2127 for _ in 0..100 {
2128 root.transaction().unwrap();
2129 }
2130
2131 assert_eq!(&*data.borrow(), &(2..102).collect::<Vec<usize>>());
2133 })
2134 .expect("failed to start runtime");
2135
2136 hruntime.join().unwrap();
2137 }
2138
2139 #[test]
2140 fn test_kill_dynamic() {
2141 test_kill::<DynamicScheduler>();
2142 }
2143
2144 fn test_kill<S>()
2146 where
2147 S: Scheduler + 'static,
2148 {
2149 let hruntime = Runtime::run(16, |_parker| {
2150 let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
2152 circuit
2153 .iterate_with_scheduler::<_, _, _, S>(|child| {
2154 let mut n: usize = 0;
2155 child
2156 .add_source(Generator::new(move || {
2157 n += 1;
2158 n
2159 }))
2160 .inspect(|_: &usize| {});
2161 Ok((async || Ok(false), ()))
2162 })
2163 .unwrap();
2164 Ok(())
2165 })
2166 .unwrap()
2167 .0;
2168
2169 loop {
2170 if root.transaction().is_err() {
2171 return;
2172 }
2173 }
2174 })
2175 .expect("failed to start runtime");
2176
2177 sleep(Duration::from_millis(100));
2178 hruntime.kill().unwrap();
2179 }
2180
2181 #[test]
2183 fn memory_pressure_thresholds_and_spill_behavior() {
2184 const GIB: u64 = 1024 * 1024 * 1024;
2185 const MIB: u64 = 1024 * 1024;
2186 const TEN_MIB: usize = 10 * 1024 * 1024;
2187
2188 let _mock_guard = MOCK_RSS_LOCK.lock().unwrap();
2189 let _clear_mock_rss = MockRssVarGuard;
2190 set_mock_process_rss_bytes(0);
2191
2192 let storage_dir = tempfile::tempdir().unwrap();
2193 let config = CircuitConfig::with_workers(1)
2194 .with_temporary_storage(storage_dir.path())
2195 .with_max_rss_bytes(Some(10 * GIB));
2196
2197 let (mut circuit, input_handle) = Runtime::init_circuit(config, move |circuit| {
2199 let (stream, input_handle) = circuit.add_input_zset::<u64>();
2200 stream.accumulate_integrate_trace();
2201 Ok(input_handle)
2202 })
2203 .unwrap();
2204
2205 for key in 0..7 {
2208 input_handle.push(key, 1);
2209 circuit.transaction().unwrap();
2210 }
2211
2212 let (pressure, min_merge, min_insert, min_step) =
2214 query_runtime_memory_state(circuit.runtime());
2215 assert_eq!(pressure, MemoryPressure::Low);
2216
2217 assert_eq!(min_merge, TEN_MIB);
2220 assert_eq!(min_insert, usize::MAX);
2221 assert_eq!(min_step, usize::MAX);
2222
2223 let profile = circuit.retrieve_profile().unwrap();
2225 assert!(in_memory_spine_records(&profile) == 7);
2226
2227 set_mock_process_rss_bytes(8800 * MIB);
2229 sleep(Duration::from_secs(5));
2230
2231 let (pressure, min_merge, min_insert, min_step) =
2232 query_runtime_memory_state(circuit.runtime());
2233 assert_eq!(pressure, MemoryPressure::Moderate);
2234 assert_eq!(min_merge, 0);
2235 assert_eq!(min_insert, TEN_MIB);
2236 assert_eq!(min_step, usize::MAX);
2237
2238 set_mock_process_rss_bytes(9400 * MIB);
2240 sleep(Duration::from_secs(5));
2241
2242 let (pressure, min_merge, min_insert, min_step) =
2243 query_runtime_memory_state(circuit.runtime());
2244 assert_eq!(pressure, MemoryPressure::High);
2245 assert_eq!(min_merge, 0);
2246 assert_eq!(min_insert, 0);
2247 assert_eq!(min_step, usize::MAX);
2248
2249 let deadline = Instant::now() + Duration::from_secs(20);
2252 loop {
2253 let profile = circuit.retrieve_profile().unwrap();
2254 if in_memory_spine_records(&profile) == 0 {
2255 break;
2256 }
2257 assert!(
2258 Instant::now() < deadline,
2259 "in-memory spine records did not drain to zero under high memory pressure"
2260 );
2261 sleep(Duration::from_millis(250));
2262 }
2263
2264 set_mock_process_rss_bytes(9800 * MIB);
2266 sleep(Duration::from_secs(3));
2267
2268 let (pressure, min_merge, min_insert, min_step) =
2269 query_runtime_memory_state(circuit.runtime());
2270 assert_eq!(pressure, MemoryPressure::Critical);
2271 assert_eq!(min_merge, 0);
2272 assert_eq!(min_insert, 0);
2273 assert_eq!(min_step, 0);
2274
2275 circuit.kill().unwrap();
2276 }
2277}