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;
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 Terminated,
83}
84
85impl DetailedError for Error {
86 fn error_code(&self) -> Cow<'static, str> {
87 match self {
88 Self::UnknownPersistentId(_) => Cow::from("UnknownPersistentId"),
89 Self::WorkerPanic { .. } => Cow::from("WorkerPanic"),
90 Self::Terminated => Cow::from("Terminated"),
91 Self::IncompatibleStorage => Cow::from("IncompatibleStorage"),
92 Self::CheckpointParseError(_) => Cow::from("CheckpointParseError"),
93 }
94 }
95}
96
97impl Display for Error {
98 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
99 match self {
100 Self::UnknownPersistentId(persistent_id) => {
101 write!(f, "Unknown persistent node id: {persistent_id}")
102 }
103 Self::WorkerPanic { panic_info } => {
104 writeln!(f, "One or more worker threads terminated unexpectedly")?;
105
106 for (worker, thread_type, worker_panic_info) in panic_info.iter() {
107 writeln!(f, "{thread_type} worker thread {worker} panicked")?;
108 writeln!(f, "{worker_panic_info}")?;
109 }
110 Ok(())
111 }
112 Self::Terminated => f.write_str("circuit has been terminated"),
113 Self::IncompatibleStorage => {
114 f.write_str("Supplied storage directory does not fit the runtime circuit")
115 }
116 Self::CheckpointParseError(error) => {
117 write!(f, "Error deserializing checkpointed state: {error}")
118 }
119 }
120 }
121}
122
123impl StdError for Error {}
124
125thread_local! {
127 static RUNTIME: RefCell<Option<Runtime>> = const { RefCell::new(None) };
130
131 static CURRENT_THREAD_TYPE: Cell<Option<ThreadType>> = const { Cell::new(None) };
134
135}
136
137thread_local! {
139 static WORKER_INDEX: Cell<usize> = const { Cell::new(0) };
143
144 static BUFFER_CACHE: RefCell<Option<Arc<BufferCache>>> = const { RefCell::new(None) };
146}
147
148tokio::task_local! {
150 pub(crate) static TOKIO_WORKER_INDEX: usize;
154
155 pub(crate) static TOKIO_BUFFER_CACHE: Arc<BufferCache>;
159}
160
161pub(crate) fn current_thread_type() -> Option<ThreadType> {
162 CURRENT_THREAD_TYPE.get()
163}
164
165fn set_current_thread_type(thread_type: ThreadType) {
166 CURRENT_THREAD_TYPE.set(Some(thread_type));
167}
168
169pub struct LocalStoreMarker;
170
171pub type LocalStore = TypedDashMap<LocalStoreMarker>;
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
176pub struct PanicLocation {
177 file: String,
178 line: u32,
179 col: u32,
180}
181
182impl Display for PanicLocation {
183 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
184 write!(f, "{}:{}:{}", self.file, self.line, self.col)
185 }
186}
187
188impl PanicLocation {
189 fn new(loc: &Location) -> Self {
190 Self {
191 file: loc.file().to_string(),
192 line: loc.line(),
193 col: loc.column(),
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct WorkerPanicInfo {
201 message: Option<String>,
203 location: Option<PanicLocation>,
205 backtrace: String,
207}
208
209impl Display for WorkerPanicInfo {
210 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
211 if let Some(message) = &self.message {
212 writeln!(f, "panic message: {message}")?;
213 } else {
214 writeln!(f, "panic message (none)")?;
215 }
216
217 if let Some(location) = &self.location {
218 writeln!(f, "panic location: {location}")?;
219 } else {
220 writeln!(f, "panic location: unknown")?;
221 }
222 writeln!(f, "stack trace:\n{}", self.backtrace)
223 }
224}
225
226impl WorkerPanicInfo {
227 fn new(panic_info: &PanicHookInfo) -> Self {
228 #[allow(clippy::manual_map)]
229 let message = if let Some(v) = panic_info.payload().downcast_ref::<String>() {
230 Some(v.clone())
231 } else if let Some(v) = panic_info.payload().downcast_ref::<&str>() {
232 Some(v.to_string())
233 } else {
234 None
235 };
236 let backtrace = Backtrace::force_capture().to_string();
237 let location = panic_info
238 .location()
239 .map(|location| PanicLocation::new(location));
240
241 Self {
242 message,
243 location,
244 backtrace,
245 }
246 }
247}
248
249#[derive(derive_more::Debug)]
250struct RuntimeStorage {
251 pub config: StorageConfig,
253
254 pub options: StorageOptions,
256
257 #[debug(skip)]
259 pub backend: Arc<dyn StorageBackend>,
260
261 #[allow(dead_code)]
263 locked_directory: LockedDirectory,
264}
265
266struct RuntimeInner {
267 layout: Layout,
268 mode: Mode,
269 step_size: StepSize,
270 dev_tweaks: DevTweaks,
271
272 max_rss: Option<u64>,
274
275 process_rss: AtomicU64,
277
278 memory_pressure: AtomicU8,
281
282 memory_pressure_epoch: AtomicU64,
284
285 memory_pressure_notify: Arc<Notify>,
287
288 storage: Option<RuntimeStorage>,
289 store: LocalStore,
290 kill_signal: AtomicBool,
291 cancellation_token: CancellationToken,
292 aux_threads: Mutex<Vec<(JoinHandle<()>, Unparker)>>,
293 buffer_caches: Vec<EnumMap<ThreadType, Arc<BufferCache>>>,
294
295 pin_cpus_fg: Vec<CoreId>,
297
298 pin_cpus_bg: Vec<CoreId>,
300 fbuf_slab_allocators: Vec<EnumMap<ThreadType, Arc<FBufSlabs>>>,
301 worker_sequence_numbers: Vec<AtomicUsize>,
302 panic_info: Vec<EnumMap<ThreadType, RwLock<Option<WorkerPanicInfo>>>>,
304 panicked: AtomicBool,
305
306 tokio_merger_runtime: Mutex<Option<TokioRuntime>>,
308
309 exchange_listener: Mutex<Option<TcpListener>>,
318}
319
320impl Drop for RuntimeInner {
321 fn drop(&mut self) {
322 debug!("dropping RuntimeInner");
323 }
324}
325
326impl Debug for RuntimeInner {
327 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
328 f.debug_struct("RuntimeInner")
329 .field("layout", &self.layout)
330 .field("storage", &self.storage)
331 .finish()
332 }
333}
334
335fn display_core_ids<'a>(iter: impl Iterator<Item = &'a CoreId>) -> String {
336 format!(
337 "{:?}",
338 iter.map(|core| core.id).collect::<Vec<_>>().as_slice()
339 )
340}
341
342fn map_pin_cpus(config: &CircuitConfig) -> (Vec<CoreId>, Vec<CoreId>) {
346 if config.layout.is_multihost() {
347 if !config.pin_cpus.is_empty() {
348 warn!("CPU pinning not yet supported with multihost DBSP");
349 }
350 return (Vec::new(), Vec::new());
351 }
352
353 let merger_threads = config.num_merger_threads();
354
355 let nworkers = config.layout.n_workers();
356 let pin_cpus = config
357 .pin_cpus
358 .iter()
359 .copied()
360 .map(|id| CoreId { id })
361 .collect::<IndexSet<_>>();
362 let expected_cpus = nworkers + merger_threads;
363 if pin_cpus.len() < expected_cpus {
364 if !pin_cpus.is_empty() {
365 warn!(
366 "ignoring CPU pinning request because {nworkers} foreground workers and {} merger workers require {} pinned CPUs but only {} were specified",
367 merger_threads,
368 expected_cpus,
369 pin_cpus.len()
370 )
371 }
372 return (Vec::new(), Vec::new());
373 }
374
375 let Some(core_ids) = get_core_ids() else {
376 warn!(
377 "ignoring CPU pinning request because this system's core ids list could not be obtained"
378 );
379 return (Vec::new(), Vec::new());
380 };
381 let core_ids = core_ids.iter().copied().collect::<IndexSet<_>>();
382
383 let missing_cpus = pin_cpus.difference(&core_ids).copied().collect::<Vec<_>>();
384 if !missing_cpus.is_empty() {
385 warn!(
386 "ignoring CPU pinning request because requested CPUs {missing_cpus:?} are not available (available CPUs are: {})",
387 display_core_ids(core_ids.iter())
388 );
389 return (Vec::new(), Vec::new());
390 }
391
392 let fg_cpus = &pin_cpus[0..nworkers];
393 let bg_cpus = &pin_cpus[nworkers..expected_cpus];
394 info!(
395 "pinning foreground workers to CPUs {} and background workers to CPUs {}",
396 display_core_ids(fg_cpus.iter()),
397 display_core_ids(bg_cpus.iter())
398 );
399 let fg_pinning = (0..nworkers).map(|i| fg_cpus[i]).collect();
400
401 let bg_pinning = (0..merger_threads).map(|i| bg_cpus[i]).collect();
402
403 (fg_pinning, bg_pinning)
404}
405
406impl RuntimeInner {
407 fn new(config: CircuitConfig) -> Result<Self, DbspError> {
408 let nworkers = config.layout.local_workers().len();
409 let buffer_cache_strategy = config.dev_tweaks.buffer_cache_strategy();
410 let buffer_max_buckets = config.dev_tweaks.buffer_max_buckets;
411 let buffer_cache_allocation_strategy = config
412 .dev_tweaks
413 .effective_buffer_cache_allocation_strategy();
414 let fbuf_slab_bytes_per_class = config
415 .dev_tweaks
416 .fbuf_slab_bytes_per_class
417 .unwrap_or(FBufSlabs::DEFAULT_BYTES_PER_CLASS);
418 let storage = if let Some(storage) = config.storage.clone() {
419 let locked_directory =
420 LockedDirectory::new_blocking(storage.config.path(), Duration::from_secs(60))?;
421 let backend = storage.backend;
422
423 if let Some(init_checkpoint) = storage.init_checkpoint
424 && !backend
425 .exists(&Checkpointer::checkpoint_dir(init_checkpoint).child("CHECKPOINT"))?
426 {
427 return Err(DbspError::Storage(StorageError::CheckpointNotFound(
428 init_checkpoint,
429 )));
430 }
431
432 Some(RuntimeStorage {
433 config: storage.config,
434 options: storage.options,
435 backend,
436 locked_directory,
437 })
438 } else {
439 None
440 };
441
442 let total_cache_bytes = if let Some(storage) = &storage {
443 match storage.options.cache_mib {
444 Some(cache_mib) => cache_mib.saturating_mul(1024 * 1024),
445 None => 256usize
446 .saturating_mul(1024 * 1024)
447 .saturating_mul(nworkers)
448 .saturating_mul(ThreadType::LENGTH),
449 }
450 } else {
451 1
453 };
454
455 info!(
456 "Setting up buffer caches: {buffer_cache_strategy:?} {buffer_cache_allocation_strategy:?} buckets={buffer_max_buckets:?} total_size={:?} MiB",
457 total_cache_bytes / (1024 * 1024)
458 );
459 let buffer_caches = build_buffer_caches(
460 nworkers,
461 total_cache_bytes,
462 buffer_cache_strategy,
463 buffer_max_buckets,
464 buffer_cache_allocation_strategy,
465 );
466
467 info!("Setting up FBuf slab allocators: bytes_per_class={fbuf_slab_bytes_per_class}");
468 let fbuf_slab_allocators = (0..nworkers)
469 .map(|_| {
470 let allocator = Arc::new(FBufSlabs::new(fbuf_slab_bytes_per_class));
471 enum_map! {
472 ThreadType::Foreground => allocator.clone(),
473 ThreadType::Background => allocator.clone(),
474 }
475 })
476 .collect();
477
478 let (pin_cpus_fg, pin_cpus_bg) = map_pin_cpus(&config);
479
480 if !config.dev_tweaks.other_options.is_empty() {
481 warn!(
482 "Circuit dev_tweaks includes unknown options: {:?}",
483 &config.dev_tweaks.other_options
484 );
485 }
486
487 if config.dev_tweaks.streaming_exchange() {
488 info!("dev_tweaks.streaming_exchange enabled");
489 } else {
490 info!("dev_tweaks.streaming_exchange disabled");
491 }
492
493 Ok(Self {
494 pin_cpus_fg,
495 pin_cpus_bg,
496 layout: config.layout,
497 mode: config.mode,
498 step_size: config.step_size,
499 dev_tweaks: config.dev_tweaks,
500 max_rss: config.max_rss_bytes,
501 process_rss: AtomicU64::new(process_rss_bytes().unwrap_or_default()),
502 memory_pressure: AtomicU8::new(MemoryPressure::Low as u8),
503 memory_pressure_epoch: AtomicU64::new(0),
504 memory_pressure_notify: Arc::new(Notify::new()),
505 storage,
506 store: TypedDashMap::new(),
507 kill_signal: AtomicBool::new(false),
508 cancellation_token: CancellationToken::new(),
509 aux_threads: Mutex::new(Vec::new()),
510 buffer_caches,
511 fbuf_slab_allocators,
512 worker_sequence_numbers: (0..nworkers).map(|_| AtomicUsize::new(0)).collect(),
513 panic_info: (0..nworkers)
514 .map(|_| EnumMap::from_fn(|_| RwLock::new(None)))
515 .collect(),
516 panicked: AtomicBool::new(false),
517 tokio_merger_runtime: Mutex::new(None),
518 exchange_listener: Mutex::new(config.exchange_listener),
519 })
520 }
521
522 fn pin_cpu(&self) {
523 match current_thread_type() {
524 Some(ThreadType::Foreground) => {
525 if !self.pin_cpus_fg.is_empty() {
526 let local_worker_offset = Runtime::local_worker_offset();
527 let core = self.pin_cpus_fg[local_worker_offset];
528 if !core_affinity::set_for_current(core) {
529 warn!(
530 "failed to pin foreground worker {local_worker_offset} to core {}",
531 core.id
532 );
533 }
534 }
535 }
536 Some(ThreadType::Background) => {
537 if !self.pin_cpus_bg.is_empty() {
538 let local_worker_offset = Runtime::local_worker_offset();
539 let core = self.pin_cpus_bg[local_worker_offset];
540 if !core_affinity::set_for_current(core) {
541 warn!(
542 "failed to pin background worker {local_worker_offset} to core {}",
543 core.id
544 );
545 }
546 }
547 }
548 None => {
549 panic!("pin_cpu() called outside of a runtime or on an aux thread");
550 }
551 }
552 }
553
554 fn memory_pressure(&self) -> MemoryPressure {
555 MemoryPressure::try_from(self.memory_pressure.load(Ordering::Relaxed)).unwrap()
556 }
557
558 fn raw_memory_pressure(&self, process_rss: u64) -> MemoryPressure {
562 let process_rss = process_rss as f64;
563
564 let Some(max_rss) = self.max_rss else {
565 return MemoryPressure::Low;
566 };
567
568 if process_rss >= (CRITICAL_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
569 return MemoryPressure::Critical;
570 }
571
572 if process_rss >= (HIGH_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
573 return MemoryPressure::High;
574 }
575
576 if process_rss >= (MODERATE_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
577 return MemoryPressure::Moderate;
578 }
579
580 MemoryPressure::Low
581 }
582}
583
584fn panic_hook(panic_info: &PanicHookInfo<'_>, default_panic_hook: &dyn Fn(&PanicHookInfo<'_>)) {
594 default_panic_hook(panic_info);
596
597 RUNTIME.with(|runtime| {
598 if let Ok(runtime) = runtime.try_borrow()
599 && let Some(runtime) = runtime.as_ref()
600 {
601 runtime.panic(panic_info);
602 }
603 })
604}
605
606#[repr(transparent)]
610#[derive(Clone, Debug)]
611pub struct Runtime(Arc<RuntimeInner>);
612
613#[repr(transparent)]
615#[derive(Clone, Debug)]
616pub struct WeakRuntime(Weak<RuntimeInner>);
617
618impl WeakRuntime {
619 pub fn upgrade(&self) -> Option<Runtime> {
620 self.0.upgrade().map(Runtime)
621 }
622}
623
624#[allow(clippy::type_complexity)]
627static DEFAULT_PANIC_HOOK: Lazy<Box<dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send>> =
628 Lazy::new(|| {
629 let _ = panic::take_hook();
631 panic::take_hook()
632 });
633
634fn default_panic_hook() -> &'static (dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send) {
636 &*DEFAULT_PANIC_HOOK
637}
638
639impl Runtime {
640 pub fn run<F>(config: impl Into<CircuitConfig>, circuit: F) -> Result<RuntimeHandle, DbspError>
679 where
680 F: FnOnce(Parker) + Clone + Send + 'static,
681 {
682 let mut config: CircuitConfig = config.into();
683 if config.step_size == StepSize::FullSteps {
684 config.dev_tweaks.splitter_chunk_size_records = Some(u64::MAX);
685 }
686
687 let workers = config.layout.local_workers();
688 let num_merger_threads = config.num_merger_threads();
689
690 let runtime = Self(Arc::new(RuntimeInner::new(config)?));
691
692 let default_hook = default_panic_hook();
694 panic::set_hook(Box::new(move |panic_info| {
695 panic_hook(panic_info, default_hook)
696 }));
697
698 let runtime_clone = runtime.clone();
700 let tokio_merger_runtime: TokioRuntime = {
701 info!("starting dbsp merger tokio runtime, workers: {num_merger_threads}",);
702
703 TokioBuilder::new_multi_thread()
704 .worker_threads(num_merger_threads)
705 .thread_name_fn(|| {
706 use std::sync::atomic::{AtomicUsize, Ordering};
707 static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
708 let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
709 format!("merger-{}", id)
710 })
711 .on_thread_start(move || {
712 set_current_thread_type(ThreadType::Background);
713 RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime_clone.clone()));
714 })
715 .thread_stack_size(6 * 1024 * 1024)
716 .enable_all()
717 .build()
718 .unwrap()
719 };
720
721 runtime
722 .inner()
723 .tokio_merger_runtime
724 .lock()
725 .unwrap()
726 .replace(tokio_merger_runtime);
727
728 runtime.spawn_aux_thread("rss-monitor", Parker::new(), |parker| {
730 let runtime = Runtime::runtime().unwrap();
731
732 let mut pressure_span = None;
733 while !Runtime::kill_in_progress() {
734 let process_rss = process_rss_bytes().unwrap_or_default();
735 runtime
736 .inner()
737 .process_rss
738 .store(process_rss, Ordering::Relaxed);
739
740 let previous_pressure = runtime.inner().memory_pressure();
741 let current_memory_pressure = runtime.inner().raw_memory_pressure(process_rss);
742
743 let new_memory_pressure = match (previous_pressure, current_memory_pressure) {
754 (MemoryPressure::Low | MemoryPressure::Moderate, _) => current_memory_pressure,
755 (MemoryPressure::High, MemoryPressure::Critical) => MemoryPressure::Critical,
756 (MemoryPressure::High, MemoryPressure::High | MemoryPressure::Moderate) => {
757 MemoryPressure::High
758 }
759 (MemoryPressure::High, MemoryPressure::Low) => MemoryPressure::Low,
760 (MemoryPressure::Critical, MemoryPressure::Critical | MemoryPressure::High) => {
761 MemoryPressure::Critical
762 }
763 (MemoryPressure::Critical, MemoryPressure::Moderate | MemoryPressure::Low) => {
764 current_memory_pressure
765 }
766 };
767 if pressure_span.is_none() || new_memory_pressure != previous_pressure {
768 pressure_span = Some(
769 LongSpanBuilder::new("memory-pressure")
770 .with_tooltip(new_memory_pressure.to_string())
771 .build(),
772 );
773 }
774
775 runtime
776 .inner()
777 .memory_pressure
778 .store(new_memory_pressure as u8, Ordering::Relaxed);
779
780 if previous_pressure < MemoryPressure::High
782 && new_memory_pressure >= MemoryPressure::High
783 {
784 runtime
785 .inner()
786 .memory_pressure_epoch
787 .fetch_add(1, Ordering::Relaxed);
788 runtime.inner().memory_pressure_notify.notify_waiters();
789 }
790 parker.park_timeout(Duration::from_secs(1));
791 }
792 drop(pressure_span);
793 });
794
795 let workers = workers
796 .map(|worker_index| {
797 let runtime = runtime.clone();
798 let build_circuit = circuit.clone();
799 let parker = Parker::new();
800 let unparker = parker.unparker().clone();
801 let local_worker_offset =
802 worker_index - runtime.inner().layout.local_workers().start;
803 let fbuf_slab_allocator =
804 runtime.get_fbuf_slab_allocator(local_worker_offset, ThreadType::Foreground);
805 let handle = Builder::new()
806 .name(format!("dbsp-worker-{worker_index}"))
807 .spawn(move || {
808 WORKER_INDEX.set(worker_index);
810 set_current_thread_type(ThreadType::Foreground);
811 set_thread_slab_pool(Some(fbuf_slab_allocator));
812 runtime.inner().pin_cpu();
813 RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
814
815 build_circuit(parker);
817 })
818 .unwrap_or_else(|error| {
819 panic!("failed to spawn worker thread {worker_index}: {error}");
820 });
821 (handle, unparker)
822 })
823 .collect::<Vec<_>>();
824
825 Ok(RuntimeHandle::new(runtime, workers))
826 }
827
828 pub fn downgrade(&self) -> WeakRuntime {
829 WeakRuntime(Arc::downgrade(&self.0))
830 }
831
832 #[allow(clippy::self_named_constructors)]
842 pub fn runtime() -> Option<Runtime> {
843 RUNTIME.with(|rt| rt.borrow().clone())
844 }
845
846 pub fn storage_backend() -> Result<Arc<dyn StorageBackend>, StorageError> {
852 Runtime::runtime()
853 .unwrap()
854 .inner()
855 .storage
856 .as_ref()
857 .map_or(Err(StorageError::StorageDisabled), |storage| {
858 Ok(storage.backend.clone())
859 })
860 }
861
862 pub fn buffer_cache() -> Option<Arc<BufferCache>> {
881 if let Some(buffer_cache) = BUFFER_CACHE.with(|bc| bc.borrow().clone()) {
882 Some(buffer_cache)
884 } else if let Ok(buffer_cache) = TOKIO_BUFFER_CACHE.try_get() {
885 Some(buffer_cache)
887 } else if let Some(rt) = Runtime::runtime()
888 && let Some(thread_type) = current_thread_type()
889 {
890 match thread_type {
892 ThreadType::Foreground => {
893 let buffer_cache =
894 rt.get_buffer_cache(Runtime::local_worker_offset(), thread_type);
895 BUFFER_CACHE.set(Some(buffer_cache.clone()));
896 Some(buffer_cache)
897 }
898 ThreadType::Background => {
899 None
905 }
906 }
907 } else {
908 static AUXILIARY_CACHE: LazyLock<Arc<BufferCache>> =
921 LazyLock::new(|| Arc::new(BufferCache::new(1024 * 1024 * 256)));
922
923 let buffer_cache = AUXILIARY_CACHE.clone();
924 BUFFER_CACHE.set(Some(buffer_cache.clone()));
925 Some(buffer_cache)
926 }
927 }
928
929 pub fn spawn_aux_thread<F>(&self, thread_name: &str, parker: Parker, f: F)
942 where
943 F: FnOnce(Parker) + Send + 'static,
944 {
945 let runtime = self.clone();
946 let unparker = parker.unparker().clone();
947 let handle = Builder::new()
948 .name(thread_name.to_string())
949 .spawn(move || {
950 RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
951 f(parker)
952 })
953 .expect("failed to spawn auxiliary thread");
954
955 self.inner()
956 .aux_threads
957 .lock()
958 .unwrap()
959 .push((handle, unparker))
960 }
961
962 pub fn get_buffer_cache(
967 &self,
968 local_worker_offset: usize,
969 thread_type: ThreadType,
970 ) -> Arc<BufferCache> {
971 self.0.buffer_caches[local_worker_offset][thread_type].clone()
972 }
973
974 pub(crate) fn get_fbuf_slab_allocator(
975 &self,
976 local_worker_offset: usize,
977 thread_type: ThreadType,
978 ) -> Arc<FBufSlabs> {
979 self.0.fbuf_slab_allocators[local_worker_offset][thread_type].clone()
980 }
981
982 pub fn cache_occupancy(&self) -> (usize, usize) {
985 if self.0.storage.is_some() {
986 let mut seen = HashSet::new();
987 self.0
988 .buffer_caches
989 .iter()
990 .flat_map(|map| map.values())
991 .filter(|cache| seen.insert(cache.backend_id()))
992 .map(|cache| cache.occupancy())
993 .fold((0, 0), |(a_cur, a_max), (b_cur, b_max)| {
994 (a_cur + b_cur, a_max + b_max)
995 })
996 } else {
997 (0, 0)
998 }
999 }
1000
1001 pub fn fbuf_slabs_stats(&self) -> FBufSlabsStats {
1003 if self.0.storage.is_some() {
1004 let mut seen = HashSet::new();
1005 self.0
1006 .fbuf_slab_allocators
1007 .iter()
1008 .flat_map(|map| map.values())
1009 .filter(|allocator| seen.insert(allocator.backend_id()))
1010 .map(|allocator| allocator.stats())
1011 .fold(FBufSlabsStats::default(), |mut stats, pool_stats| {
1012 stats += pool_stats;
1013 stats
1014 })
1015 } else {
1016 FBufSlabsStats::default()
1017 }
1018 }
1019
1020 pub fn worker_index() -> usize {
1027 match current_thread_type() {
1028 Some(ThreadType::Foreground) => WORKER_INDEX.get(),
1029 Some(ThreadType::Background) => TOKIO_WORKER_INDEX.get(),
1030 None => RUNTIME.with_borrow(|runtime| match runtime {
1031 Some(runtime) => {
1032 runtime.layout().local_workers().start
1035 }
1036 None => {
1037 0
1039 }
1040 }),
1041 }
1042 }
1043
1044 pub fn local_worker_offset() -> usize {
1046 let local_workers_start = RUNTIME
1048 .with(|rt| Some(rt.borrow().as_ref()?.layout().local_workers().start))
1049 .unwrap_or_default();
1050 Self::worker_index() - local_workers_start
1051 }
1052
1053 pub fn mode() -> Mode {
1054 RUNTIME
1055 .with(|rt| Some(rt.borrow().as_ref()?.get_mode()))
1056 .unwrap_or_default()
1057 }
1058
1059 pub fn with_dev_tweaks<F, T>(f: F) -> T
1060 where
1061 F: Fn(&DevTweaks) -> T,
1062 {
1063 static DEFAULT: Lazy<DevTweaks> = Lazy::new(DevTweaks::default);
1064 RUNTIME
1065 .with(|rt| Some(f(&rt.borrow().as_ref()?.inner().dev_tweaks)))
1066 .unwrap_or_else(|| f(&DEFAULT))
1067 }
1068
1069 pub fn get_mode(&self) -> Mode {
1070 self.inner().mode
1071 }
1072
1073 pub fn get_step_size(&self) -> StepSize {
1074 self.inner().step_size
1075 }
1076
1077 pub fn worker_index_str() -> &'static str {
1081 static WORKER_INDEX_STRS: Lazy<[&'static str; 256]> = Lazy::new(|| {
1082 let mut data: [&'static str; 256] = [""; 256];
1083 for (i, item) in data.iter_mut().enumerate() {
1084 *item = Box::leak(i.to_string().into_boxed_str());
1085 }
1086 data
1087 });
1088
1089 WORKER_INDEX_STRS
1090 .get(Runtime::worker_index())
1091 .copied()
1092 .unwrap_or_else(|| {
1093 panic!("Limit workers to less than 256 or increase the limit in the code.")
1094 })
1095 }
1096
1097 pub fn memory_pressure() -> Option<MemoryPressure> {
1098 RUNTIME.with(|rt| Some(rt.borrow().as_ref()?.inner().memory_pressure()))
1099 }
1100
1101 pub fn current_memory_pressure(&self) -> MemoryPressure {
1102 self.inner().memory_pressure()
1103 }
1104
1105 pub fn max_rss_bytes(&self) -> Option<u64> {
1106 self.inner().max_rss
1107 }
1108
1109 pub fn memory_pressure_epoch(&self) -> u64 {
1110 self.inner().memory_pressure_epoch.load(Ordering::Relaxed)
1111 }
1112
1113 pub(crate) fn memory_pressure_notify(&self) -> Arc<Notify> {
1114 self.inner().memory_pressure_notify.clone()
1115 }
1116
1117 pub fn min_merge_storage_bytes() -> Option<usize> {
1129 RUNTIME.with(|rt| {
1130 let rt = rt.borrow();
1131 let inner = rt.as_ref()?.inner();
1132 let storage = inner.storage.as_ref()?;
1133
1134 if inner.memory_pressure() >= MemoryPressure::Moderate {
1135 Some(0)
1136 } else {
1137 Some(storage.options.min_storage_bytes.unwrap_or({
1138 10 * 1024 * 1024
1141 }))
1142 }
1143 })
1144 }
1145
1146 pub fn min_insert_storage_bytes() -> Option<usize> {
1158 RUNTIME.with(|rt| {
1159 let rt = rt.borrow();
1160 let inner = rt.as_ref()?.inner();
1161 let storage = inner.storage.as_ref()?;
1162
1163 if inner.memory_pressure() >= MemoryPressure::High {
1164 Some(0)
1165 } else if inner.memory_pressure() >= MemoryPressure::Moderate {
1166 Some(
1168 storage
1169 .options
1170 .min_storage_bytes
1171 .unwrap_or(10 * 1024 * 1024),
1172 )
1173 } else {
1174 Some(usize::MAX)
1177 }
1178 })
1179 }
1180
1181 pub fn min_step_storage_bytes() -> Option<usize> {
1194 RUNTIME.with(|rt| {
1195 let rt = rt.borrow();
1196 let inner = rt.as_ref()?.inner();
1197 let storage = inner.storage.as_ref()?;
1198
1199 if inner.memory_pressure() >= MemoryPressure::Critical {
1200 Some(0)
1201 } else {
1202 Some(storage.options.min_step_storage_bytes.unwrap_or(usize::MAX))
1203 }
1204 })
1205 }
1206
1207 pub fn file_writer_parameters() -> Parameters {
1208 let compression = Runtime::runtime()
1209 .unwrap()
1210 .inner()
1211 .storage
1212 .as_ref()
1213 .unwrap()
1214 .options
1215 .compression;
1216 let compression = match compression {
1217 StorageCompression::Default | StorageCompression::Snappy => Some(Compression::Snappy),
1218 StorageCompression::None => None,
1219 };
1220 Parameters::default().with_compression(compression)
1221 }
1222
1223 fn inner(&self) -> &RuntimeInner {
1224 &self.0
1225 }
1226
1227 pub fn num_workers() -> usize {
1232 RUNTIME.with(|rt| {
1233 rt.borrow()
1234 .as_ref()
1235 .map_or(1, |runtime| runtime.layout().n_workers())
1236 })
1237 }
1238
1239 pub fn layout(&self) -> &Layout {
1241 &self.inner().layout
1242 }
1243
1244 pub fn local_store(&self) -> &LocalStore {
1255 &self.inner().store
1256 }
1257
1258 pub fn storage_path(&self) -> Option<&Path> {
1260 self.inner()
1261 .storage
1262 .as_ref()
1263 .map(|storage| storage.config.path())
1264 }
1265
1266 pub fn sequence_next(&self) -> usize {
1272 self.inner().worker_sequence_numbers[Self::local_worker_offset()]
1273 .fetch_add(1, Ordering::Relaxed)
1274 }
1275
1276 pub fn kill_in_progress() -> bool {
1280 RUNTIME.with(|runtime| {
1284 runtime
1285 .borrow()
1286 .as_ref()
1287 .map(|runtime| runtime.inner().kill_signal.load(Ordering::SeqCst))
1288 .unwrap_or(false)
1289 })
1290 }
1291
1292 pub fn cancellation_token(&self) -> CancellationToken {
1293 self.inner().cancellation_token.clone()
1294 }
1295
1296 pub fn worker_panic_info(
1297 &self,
1298 worker: usize,
1299 thread_type: ThreadType,
1300 ) -> Option<WorkerPanicInfo> {
1301 if let Ok(guard) = self.inner().panic_info[worker][thread_type].read() {
1302 guard.clone()
1303 } else {
1304 warn!("poisoned panic_lock lock for {thread_type} worker {worker}");
1305 None
1306 }
1307 }
1308
1309 fn panic(&self, panic_info: &PanicHookInfo) {
1311 let local_worker_offset = Self::local_worker_offset();
1312 let Some(thread_type) = current_thread_type() else {
1313 error!("panic hook called outside of a runtime or on an aux thread");
1316 return;
1317 };
1318 let panic_info = WorkerPanicInfo::new(panic_info);
1319 let _ = self.inner().panic_info[local_worker_offset][thread_type]
1320 .write()
1321 .map(|mut guard| *guard = Some(panic_info));
1322 self.inner().panicked.store(true, Ordering::Release);
1323 }
1324
1325 pub(crate) fn tokio_merger_runtime(&self) -> Option<tokio::runtime::Handle> {
1332 self.inner()
1333 .tokio_merger_runtime
1334 .lock()
1335 .unwrap()
1336 .as_ref()
1337 .map(|rt| rt.handle().clone())
1338 }
1339
1340 pub(crate) fn take_exchange_listener(&self) -> Option<TcpListener> {
1345 self.inner().exchange_listener.lock().unwrap().take()
1346 }
1347}
1348
1349pub(crate) struct Consensus(Broadcast<bool>);
1352
1353impl Consensus {
1354 pub fn new(name: impl Display) -> Self {
1355 Self(Broadcast::new(name))
1356 }
1357
1358 pub async fn check(&self, local: bool) -> Result<bool, SchedulerError> {
1364 Ok(self.0.collect(local).await?.into_iter().all(identity))
1365 }
1366}
1367
1368pub(crate) enum Broadcast<T> {
1371 SingleThreaded,
1372 MultiThreaded {
1373 exchange: Arc<Exchange<T>>,
1374 identifier: Arc<String>,
1375 },
1376}
1377
1378impl<T> Broadcast<T>
1379where
1380 T: Clone + Debug + Send + serde::Serialize + for<'de> serde::Deserialize<'de> + 'static,
1381{
1382 pub fn new(name: impl Display) -> Self {
1383 match Runtime::runtime() {
1384 Some(runtime) if Runtime::num_workers() > 1 => {
1385 let exchange_id = runtime.sequence_next().try_into().unwrap();
1386 let exchange = Exchange::with_runtime(&runtime, exchange_id);
1387 let identifier = Arc::new(format!("broadcast {name} (exchange {exchange_id})"));
1388
1389 Self::MultiThreaded {
1390 exchange,
1391 identifier,
1392 }
1393 }
1394 _ => Self::SingleThreaded,
1395 }
1396 }
1397
1398 pub async fn collect(&self, local: T) -> Result<Vec<T>, SchedulerError> {
1404 match self {
1405 Self::SingleThreaded => Ok(vec![local]),
1406 Self::MultiThreaded {
1407 exchange,
1408 identifier,
1409 } => {
1410 let _span = Span::new("broadcast")
1413 .with_category("Exchange")
1414 .with_tooltip(|| format!("collect for {}", identifier));
1415
1416 Runtime::runtime()
1417 .unwrap()
1418 .cancellation_token()
1419 .run_until_cancelled_owned(async {
1420 exchange
1421 .send_all_with_serializer(identifier, repeat(local.clone()), |local| {
1422 let mut fbuf = FBuf::new();
1423 serde_json::to_writer(&mut fbuf, &local).unwrap();
1424 fbuf
1425 })
1426 .await;
1427
1428 exchange
1429 .receive_all(|data| serde_json::from_slice(&data).unwrap())
1430 .await
1431 })
1432 .await
1433 .ok_or(SchedulerError::Killed)
1434 }
1435 }
1436 }
1437}
1438
1439#[derive(Debug)]
1441pub struct RuntimeHandle {
1442 runtime: Runtime,
1443 workers: Vec<(JoinHandle<()>, Unparker)>,
1444}
1445
1446impl RuntimeHandle {
1447 fn new(runtime: Runtime, workers: Vec<(JoinHandle<()>, Unparker)>) -> Self {
1448 Self { runtime, workers }
1449 }
1450
1451 pub(super) fn unpark_worker(&self, worker: usize) {
1457 self.workers[worker].1.unpark();
1458 }
1459
1460 pub fn runtime(&self) -> &Runtime {
1462 &self.runtime
1463 }
1464
1465 pub fn kill(self) -> ThreadResult<()> {
1473 self.kill_async();
1474 self.join()
1475 }
1476
1477 pub fn kill_async(&self) {
1480 self.runtime
1481 .inner()
1482 .kill_signal
1483 .store(true, Ordering::SeqCst);
1484 self.runtime.inner().cancellation_token.cancel();
1485 for (_worker, unparker) in self.workers.iter() {
1486 unparker.unpark();
1487 }
1488
1489 self.runtime
1490 .inner()
1491 .aux_threads
1492 .lock()
1493 .unwrap()
1494 .iter()
1495 .for_each(|(_h, unparker)| {
1496 unparker.unpark();
1497 });
1498 }
1499
1500 pub fn join(self) -> ThreadResult<()> {
1504 #[allow(clippy::needless_collect)]
1506 let results: Vec<ThreadResult<()>> = self
1507 .workers
1508 .into_iter()
1509 .map(|(h, _unparker)| h.join())
1510 .collect();
1511
1512 self.runtime
1517 .inner()
1518 .kill_signal
1519 .store(true, Ordering::SeqCst);
1520
1521 self.runtime
1522 .inner()
1523 .aux_threads
1524 .lock()
1525 .unwrap()
1526 .iter()
1527 .for_each(|(_h, unparker)| {
1528 unparker.unpark();
1529 });
1530
1531 self.runtime
1533 .inner()
1534 .aux_threads
1535 .lock()
1536 .unwrap()
1537 .drain(..)
1538 .for_each(|(h, _unparker)| {
1539 let _ = h.join();
1540 });
1541
1542 let tokio_merger_runtime = self
1552 .runtime
1553 .inner()
1554 .tokio_merger_runtime
1555 .lock()
1556 .unwrap()
1557 .take();
1558 drop(tokio_merger_runtime);
1559
1560 self.runtime.local_store().clear();
1565
1566 results.into_iter().collect()
1567 }
1568
1569 pub fn worker_panic_info(
1571 &self,
1572 worker: usize,
1573 thread_type: ThreadType,
1574 ) -> Option<WorkerPanicInfo> {
1575 self.runtime.worker_panic_info(worker, thread_type)
1576 }
1577
1578 pub fn collect_panic_info(&self) -> Vec<(usize, ThreadType, WorkerPanicInfo)> {
1580 let mut result = Vec::new();
1581
1582 for worker in 0..self.workers.len() {
1583 for thread_type in [ThreadType::Foreground, ThreadType::Background] {
1584 if let Some(panic_info) = self.worker_panic_info(worker, thread_type) {
1585 result.push((worker, thread_type, panic_info))
1586 }
1587 }
1588 }
1589 result
1590 }
1591
1592 pub fn panicked(&self) -> bool {
1594 self.runtime.inner().panicked.load(Ordering::Acquire)
1595 }
1596}
1597
1598#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1600pub enum WorkerLocation {
1601 Local,
1603 Remote,
1605}
1606
1607#[derive(Clone, Debug)]
1611pub struct WorkerLocations {
1612 workers: Range<usize>,
1613 local_workers: Range<usize>,
1614}
1615
1616impl Default for WorkerLocations {
1617 fn default() -> Self {
1618 Self::new()
1619 }
1620}
1621
1622impl WorkerLocations {
1623 pub fn new() -> Self {
1627 if let Some(runtime) = Runtime::runtime() {
1628 Self::for_layout(runtime.layout())
1629 } else {
1630 Self {
1631 workers: 0..1,
1632 local_workers: 0..1,
1633 }
1634 }
1635 }
1636
1637 pub fn for_layout(layout: &Layout) -> Self {
1639 Self {
1640 workers: 0..layout.n_workers(),
1641 local_workers: layout.local_workers(),
1642 }
1643 }
1644
1645 pub fn get(&self, worker: usize) -> WorkerLocation {
1647 self[worker]
1648 }
1649}
1650
1651impl Index<usize> for WorkerLocations {
1652 type Output = WorkerLocation;
1653
1654 fn index(&self, worker: usize) -> &Self::Output {
1655 debug_assert!(worker < self.workers.end);
1656 if self.local_workers.contains(&worker) {
1657 &WorkerLocation::Local
1658 } else {
1659 &WorkerLocation::Remote
1660 }
1661 }
1662}
1663
1664impl Iterator for WorkerLocations {
1665 type Item = WorkerLocation;
1666
1667 fn next(&mut self) -> Option<Self::Item> {
1668 let worker = self.workers.next()?;
1669 Some(self.get(worker))
1670 }
1671
1672 fn size_hint(&self) -> (usize, Option<usize>) {
1673 let len = self.workers.len();
1674 (len, Some(len))
1675 }
1676}
1677
1678impl ExactSizeIterator for WorkerLocations {}
1679
1680#[cfg(test)]
1681mod tests {
1682 use super::{Runtime, RuntimeInner};
1683 use crate::{
1684 Circuit, RootCircuit,
1685 circuit::{
1686 CircuitConfig,
1687 dbsp_handle::CircuitStorageConfig,
1688 metadata::{LOOSE_MEMORY_RECORDS_COUNT, MERGING_MEMORY_RECORDS_COUNT},
1689 schedule::{DynamicScheduler, Scheduler},
1690 },
1691 operator::Generator,
1692 profile::DbspProfile,
1693 storage::backend::FileId,
1694 };
1695 use enum_map::Enum;
1696 use feldera_buffer_cache::{CacheEntry, ThreadType};
1697 use feldera_storage::fbuf::{FBuf, slab::set_thread_slab_pool};
1698 use feldera_types::config::{
1699 StorageCacheConfig, StorageConfig, StorageOptions,
1700 dev_tweaks::{BufferCacheAllocationStrategy, BufferCacheStrategy},
1701 };
1702 use feldera_types::memory_pressure::MemoryPressure;
1703 use std::{
1704 cell::RefCell,
1705 rc::Rc,
1706 sync::{Arc, LazyLock, Mutex},
1707 thread::sleep,
1708 time::{Duration, Instant},
1709 };
1710
1711 struct TestCacheEntry(usize);
1712
1713 impl CacheEntry for TestCacheEntry {
1714 fn cost(&self) -> usize {
1715 self.0
1716 }
1717 }
1718
1719 static MOCK_RSS_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1721
1722 fn set_mock_process_rss_bytes(bytes: u64) {
1723 unsafe {
1725 std::env::set_var("MOCK_PROCESS_RSS_BYTES", bytes.to_string());
1726 }
1727 }
1728
1729 struct MockRssVarGuard;
1730
1731 impl Drop for MockRssVarGuard {
1732 fn drop(&mut self) {
1733 unsafe {
1735 std::env::remove_var("MOCK_PROCESS_RSS_BYTES");
1736 }
1737 }
1738 }
1739
1740 fn query_runtime_memory_state(runtime: &Runtime) -> (MemoryPressure, usize, usize, usize) {
1741 let (sender, receiver) = std::sync::mpsc::channel();
1742 runtime.spawn_aux_thread(
1743 "memory-pressure-test-query",
1744 super::Parker::new(),
1745 move |_parker| {
1746 let _ = sender.send((
1747 Runtime::memory_pressure().expect("query thread should run inside runtime"),
1748 Runtime::min_merge_storage_bytes().expect("runtime has storage configured"),
1749 Runtime::min_insert_storage_bytes().expect("runtime has storage configured"),
1750 Runtime::min_step_storage_bytes().expect("runtime has storage configured"),
1751 ));
1752 },
1753 );
1754 receiver
1755 .recv_timeout(Duration::from_secs(5))
1756 .expect("failed to query memory state from runtime thread")
1757 }
1758
1759 fn count_metric(
1760 profile: &DbspProfile,
1761 metric: &'static crate::circuit::metadata::MetricId,
1762 ) -> usize {
1763 let root_node = crate::circuit::GlobalNodeId::root();
1764 profile
1765 .worker_profiles
1766 .iter()
1767 .map(|worker| {
1768 worker
1769 .get_node_profile(&root_node)
1770 .map(|meta| {
1771 meta.iter()
1772 .filter_map(|((metric_id, _labels), value)| {
1773 if metric_id == metric {
1774 match value {
1775 crate::circuit::metadata::MetaItem::Count(count) => {
1776 Some(*count)
1777 }
1778 _ => None,
1779 }
1780 } else {
1781 None
1782 }
1783 })
1784 .sum::<usize>()
1785 })
1786 .unwrap_or(0)
1787 })
1788 .sum()
1789 }
1790
1791 fn in_memory_spine_records(profile: &DbspProfile) -> usize {
1792 count_metric(profile, &LOOSE_MEMORY_RECORDS_COUNT)
1793 + count_metric(profile, &MERGING_MEMORY_RECORDS_COUNT)
1794 }
1795
1796 #[test]
1797 #[cfg_attr(miri, ignore)]
1798 fn test_runtime_dynamic() {
1799 test_runtime::<DynamicScheduler>();
1800 }
1801
1802 #[test]
1803 #[cfg_attr(miri, ignore)]
1804 fn storage_no_cleanup() {
1805 let path = tempfile::tempdir().unwrap().keep();
1807 let path_clone = path.clone();
1808 let cconf = CircuitConfig::with_workers(4).with_storage(Some(
1809 CircuitStorageConfig::for_config(
1810 StorageConfig {
1811 path: path.to_string_lossy().into_owned(),
1812 cache: StorageCacheConfig::default(),
1813 },
1814 StorageOptions::default(),
1815 )
1816 .unwrap(),
1817 ));
1818
1819 let hruntime = Runtime::run(cconf, move |_parker| {
1820 let runtime = Runtime::runtime().unwrap();
1821 assert_eq!(runtime.storage_path(), Some(path_clone.as_ref()));
1822 })
1823 .expect("failed to start runtime");
1824 hruntime.join().unwrap();
1825 assert!(path.exists(), "persistent storage is not cleaned up");
1826 }
1827
1828 #[test]
1829 fn s3_fifo_is_the_default_buffer_cache_strategy() {
1830 let inner = RuntimeInner::new(CircuitConfig::with_workers(1)).unwrap();
1831 assert_eq!(
1832 inner.buffer_caches[0][ThreadType::Foreground].strategy(),
1833 BufferCacheStrategy::S3Fifo
1834 );
1835 }
1836
1837 #[test]
1838 fn default_s3_fifo_cache_shares_backend_per_worker_pair() {
1839 let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
1840 assert!(
1841 inner.buffer_caches[0][ThreadType::Foreground]
1842 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1843 );
1844 assert!(
1845 inner.buffer_caches[1][ThreadType::Foreground]
1846 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1847 );
1848 assert!(
1849 !inner.buffer_caches[0][ThreadType::Foreground]
1850 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground])
1851 );
1852 }
1853
1854 #[test]
1855 fn lru_can_still_be_selected_explicitly() {
1856 let inner = RuntimeInner::new(
1857 CircuitConfig::with_workers(1).with_buffer_cache_strategy(BufferCacheStrategy::Lru),
1858 )
1859 .unwrap();
1860 assert_eq!(
1861 inner.buffer_caches[0][ThreadType::Foreground].strategy(),
1862 BufferCacheStrategy::Lru
1863 );
1864 assert!(
1865 !inner.buffer_caches[0][ThreadType::Foreground]
1866 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1867 );
1868 }
1869
1870 #[test]
1871 fn lru_uses_default_total_cache_capacity_when_cache_mib_is_unset() {
1872 let workers = 3usize;
1873 let path = tempfile::tempdir().unwrap();
1874 let storage = CircuitStorageConfig::for_config(
1875 StorageConfig {
1876 path: path.path().to_string_lossy().into_owned(),
1877 cache: StorageCacheConfig::default(),
1878 },
1879 StorageOptions::default(),
1880 )
1881 .unwrap();
1882 let runtime = Runtime(Arc::new(
1883 RuntimeInner::new(
1884 CircuitConfig::with_workers(workers)
1885 .with_storage(Some(storage))
1886 .with_buffer_cache_strategy(BufferCacheStrategy::Lru),
1887 )
1888 .unwrap(),
1889 ));
1890
1891 assert_eq!(
1892 runtime.cache_occupancy(),
1893 (0, workers * ThreadType::LENGTH * 256usize * 1024 * 1024)
1894 );
1895 }
1896
1897 #[test]
1898 fn s3_fifo_cache_can_share_cache_per_worker_pair_when_requested() {
1899 let config = CircuitConfig::with_workers(2).with_buffer_cache_allocation_strategy(
1900 BufferCacheAllocationStrategy::SharedPerWorkerPair,
1901 );
1902 let inner = RuntimeInner::new(config).unwrap();
1903 assert!(
1904 inner.buffer_caches[0][ThreadType::Foreground]
1905 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1906 );
1907 assert!(
1908 inner.buffer_caches[1][ThreadType::Foreground]
1909 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1910 );
1911 }
1912
1913 #[test]
1914 fn fbuf_slabs_are_shared_per_worker_pair() {
1915 let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
1916 assert!(Arc::ptr_eq(
1917 &inner.fbuf_slab_allocators[0][ThreadType::Foreground],
1918 &inner.fbuf_slab_allocators[0][ThreadType::Background],
1919 ));
1920 assert!(Arc::ptr_eq(
1921 &inner.fbuf_slab_allocators[1][ThreadType::Foreground],
1922 &inner.fbuf_slab_allocators[1][ThreadType::Background],
1923 ));
1924 assert!(!Arc::ptr_eq(
1925 &inner.fbuf_slab_allocators[0][ThreadType::Foreground],
1926 &inner.fbuf_slab_allocators[1][ThreadType::Foreground],
1927 ));
1928 }
1929
1930 #[test]
1931 fn fbuf_slabs_honor_bytes_per_class_dev_tweak() {
1932 let inner =
1933 RuntimeInner::new(CircuitConfig::with_workers(1).with_fbuf_slab_bytes_per_class(4096))
1934 .unwrap();
1935 let allocator = inner.fbuf_slab_allocators[0][ThreadType::Foreground].clone();
1936 let previous = set_thread_slab_pool(Some(allocator.clone()));
1937
1938 let first = FBuf::with_capacity(4096);
1939 let second = FBuf::with_capacity(4096);
1940 drop(first);
1941 drop(second);
1942
1943 set_thread_slab_pool(previous);
1944
1945 assert_eq!(allocator.stats().cached_buffers, 1);
1946 }
1947
1948 #[test]
1949 fn sieve_can_share_one_global_cache_when_requested() {
1950 let config = CircuitConfig::with_workers(2)
1951 .with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global);
1952 let inner = RuntimeInner::new(config).unwrap();
1953 let global = inner.buffer_caches[0][ThreadType::Foreground].clone();
1954 assert!(global.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background]));
1955 assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground]));
1956 assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background]));
1957 }
1958
1959 #[test]
1960 fn lru_keeps_separate_foreground_and_background_caches() {
1961 let config = CircuitConfig::with_workers(2)
1962 .with_buffer_cache_strategy(BufferCacheStrategy::Lru)
1963 .with_buffer_cache_allocation_strategy(
1964 BufferCacheAllocationStrategy::SharedPerWorkerPair,
1965 );
1966 let inner = RuntimeInner::new(config).unwrap();
1967 assert!(
1968 !inner.buffer_caches[0][ThreadType::Foreground]
1969 .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1970 );
1971 assert!(
1972 !inner.buffer_caches[1][ThreadType::Foreground]
1973 .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1974 );
1975 }
1976
1977 #[test]
1978 fn shared_sharded_cache_occupancy_is_not_double_counted() {
1979 let path = tempfile::tempdir().unwrap();
1980 let storage = CircuitStorageConfig::for_config(
1981 StorageConfig {
1982 path: path.path().to_string_lossy().into_owned(),
1983 cache: StorageCacheConfig::default(),
1984 },
1985 StorageOptions {
1986 cache_mib: Some(8),
1987 ..StorageOptions::default()
1988 },
1989 )
1990 .unwrap();
1991 let runtime = Runtime(Arc::new(
1992 RuntimeInner::new(
1993 CircuitConfig::with_workers(1)
1994 .with_storage(Some(storage))
1995 .with_buffer_cache_allocation_strategy(
1996 BufferCacheAllocationStrategy::SharedPerWorkerPair,
1997 ),
1998 )
1999 .unwrap(),
2000 ));
2001
2002 runtime.get_buffer_cache(0, ThreadType::Foreground).insert(
2003 FileId::new(),
2004 0,
2005 Arc::new(TestCacheEntry(1024)),
2006 );
2007
2008 assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
2009 }
2010
2011 #[test]
2012 fn global_sharded_cache_occupancy_is_not_double_counted() {
2013 let path = tempfile::tempdir().unwrap();
2014 let storage = CircuitStorageConfig::for_config(
2015 StorageConfig {
2016 path: path.path().to_string_lossy().into_owned(),
2017 cache: StorageCacheConfig::default(),
2018 },
2019 StorageOptions {
2020 cache_mib: Some(8),
2021 ..StorageOptions::default()
2022 },
2023 )
2024 .unwrap();
2025 let runtime = Runtime(Arc::new(
2026 RuntimeInner::new(
2027 CircuitConfig::with_workers(2)
2028 .with_storage(Some(storage))
2029 .with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global),
2030 )
2031 .unwrap(),
2032 ));
2033
2034 runtime.get_buffer_cache(1, ThreadType::Background).insert(
2035 FileId::new(),
2036 0,
2037 Arc::new(TestCacheEntry(1024)),
2038 );
2039
2040 assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
2041 }
2042
2043 #[test]
2044 fn shared_fbuf_slab_stats_are_not_double_counted() {
2045 let path = tempfile::tempdir().unwrap();
2046 let storage = CircuitStorageConfig::for_config(
2047 StorageConfig {
2048 path: path.path().to_string_lossy().into_owned(),
2049 cache: StorageCacheConfig::default(),
2050 },
2051 StorageOptions::default(),
2052 )
2053 .unwrap();
2054 let runtime = Runtime(Arc::new(
2055 RuntimeInner::new(CircuitConfig::with_workers(1).with_storage(Some(storage))).unwrap(),
2056 ));
2057
2058 let previous = set_thread_slab_pool(Some(
2059 runtime.get_fbuf_slab_allocator(0, ThreadType::Foreground),
2060 ));
2061
2062 let first = FBuf::with_capacity(4096);
2063 drop(first);
2064 let second = FBuf::with_capacity(3000);
2065 drop(second);
2066
2067 set_thread_slab_pool(previous);
2068
2069 let stats = runtime.fbuf_slabs_stats();
2070 let class = stats
2071 .classes
2072 .iter()
2073 .find(|class| class.size == 4096)
2074 .unwrap();
2075
2076 assert_eq!(stats.alloc_requests(), 2);
2077 assert_eq!(stats.mallocs_saved(), 1);
2078 assert_eq!(stats.frees_saved(), 2);
2079 assert_eq!(stats.cached_buffers, 1);
2080 assert_eq!(class.alloc_requests, 2);
2081 assert_eq!(class.alloc_hits, 1);
2082 assert_eq!(class.recycle_requests, 2);
2083 assert_eq!(class.recycle_hits, 2);
2084 }
2085
2086 fn test_runtime<S>()
2087 where
2088 S: Scheduler + 'static,
2089 {
2090 let hruntime = Runtime::run(4, |_parker| {
2091 let data = Rc::new(RefCell::new(vec![]));
2092 let data_clone = data.clone();
2093 let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
2094 let runtime = Runtime::runtime().unwrap();
2095 circuit
2097 .add_source(Generator::new(move || runtime.sequence_next()))
2098 .inspect(move |n: &usize| data_clone.borrow_mut().push(*n));
2099 Ok(())
2100 })
2101 .unwrap()
2102 .0;
2103
2104 for _ in 0..100 {
2105 root.transaction().unwrap();
2106 }
2107
2108 assert_eq!(&*data.borrow(), &(2..102).collect::<Vec<usize>>());
2110 })
2111 .expect("failed to start runtime");
2112
2113 hruntime.join().unwrap();
2114 }
2115
2116 #[test]
2117 fn test_kill_dynamic() {
2118 test_kill::<DynamicScheduler>();
2119 }
2120
2121 fn test_kill<S>()
2123 where
2124 S: Scheduler + 'static,
2125 {
2126 let hruntime = Runtime::run(16, |_parker| {
2127 let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
2129 circuit
2130 .iterate_with_scheduler::<_, _, _, S>(|child| {
2131 let mut n: usize = 0;
2132 child
2133 .add_source(Generator::new(move || {
2134 n += 1;
2135 n
2136 }))
2137 .inspect(|_: &usize| {});
2138 Ok((async || Ok(false), ()))
2139 })
2140 .unwrap();
2141 Ok(())
2142 })
2143 .unwrap()
2144 .0;
2145
2146 loop {
2147 if root.transaction().is_err() {
2148 return;
2149 }
2150 }
2151 })
2152 .expect("failed to start runtime");
2153
2154 sleep(Duration::from_millis(100));
2155 hruntime.kill().unwrap();
2156 }
2157
2158 #[test]
2160 fn memory_pressure_thresholds_and_spill_behavior() {
2161 const GIB: u64 = 1024 * 1024 * 1024;
2162 const MIB: u64 = 1024 * 1024;
2163 const TEN_MIB: usize = 10 * 1024 * 1024;
2164
2165 let _mock_guard = MOCK_RSS_LOCK.lock().unwrap();
2166 let _clear_mock_rss = MockRssVarGuard;
2167 set_mock_process_rss_bytes(0);
2168
2169 let storage_dir = tempfile::tempdir().unwrap();
2170 let config = CircuitConfig::with_workers(1)
2171 .with_temporary_storage(storage_dir.path())
2172 .with_max_rss_bytes(Some(10 * GIB));
2173
2174 let (mut circuit, input_handle) = Runtime::init_circuit(config, move |circuit| {
2176 let (stream, input_handle) = circuit.add_input_zset::<u64>();
2177 stream.accumulate_integrate_trace();
2178 Ok(input_handle)
2179 })
2180 .unwrap();
2181
2182 for key in 0..7 {
2185 input_handle.push(key, 1);
2186 circuit.transaction().unwrap();
2187 }
2188
2189 let (pressure, min_merge, min_insert, min_step) =
2191 query_runtime_memory_state(circuit.runtime());
2192 assert_eq!(pressure, MemoryPressure::Low);
2193
2194 assert_eq!(min_merge, TEN_MIB);
2197 assert_eq!(min_insert, usize::MAX);
2198 assert_eq!(min_step, usize::MAX);
2199
2200 let profile = circuit.retrieve_profile().unwrap();
2202 assert!(in_memory_spine_records(&profile) == 7);
2203
2204 set_mock_process_rss_bytes(8800 * MIB);
2206 sleep(Duration::from_secs(5));
2207
2208 let (pressure, min_merge, min_insert, min_step) =
2209 query_runtime_memory_state(circuit.runtime());
2210 assert_eq!(pressure, MemoryPressure::Moderate);
2211 assert_eq!(min_merge, 0);
2212 assert_eq!(min_insert, TEN_MIB);
2213 assert_eq!(min_step, usize::MAX);
2214
2215 set_mock_process_rss_bytes(9400 * MIB);
2217 sleep(Duration::from_secs(5));
2218
2219 let (pressure, min_merge, min_insert, min_step) =
2220 query_runtime_memory_state(circuit.runtime());
2221 assert_eq!(pressure, MemoryPressure::High);
2222 assert_eq!(min_merge, 0);
2223 assert_eq!(min_insert, 0);
2224 assert_eq!(min_step, usize::MAX);
2225
2226 let deadline = Instant::now() + Duration::from_secs(20);
2229 loop {
2230 let profile = circuit.retrieve_profile().unwrap();
2231 if in_memory_spine_records(&profile) == 0 {
2232 break;
2233 }
2234 assert!(
2235 Instant::now() < deadline,
2236 "in-memory spine records did not drain to zero under high memory pressure"
2237 );
2238 sleep(Duration::from_millis(250));
2239 }
2240
2241 set_mock_process_rss_bytes(9800 * MIB);
2243 sleep(Duration::from_secs(3));
2244
2245 let (pressure, min_merge, min_insert, min_step) =
2246 query_runtime_memory_state(circuit.runtime());
2247 assert_eq!(pressure, MemoryPressure::Critical);
2248 assert_eq!(min_merge, 0);
2249 assert_eq!(min_insert, 0);
2250 assert_eq!(min_step, 0);
2251
2252 circuit.kill().unwrap();
2253 }
2254}