Skip to main content

dbsp/circuit/
runtime.rs

1//! A multithreaded runtime for evaluating DBSP circuits in a data-parallel
2//! fashion.
3
4use 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    /// Specified persistent id is not found in the circuit.
71    UnknownPersistentId(String),
72    /// One of the worker threads terminated unexpectedly.
73    WorkerPanic {
74        // Detailed panic information from all threads that
75        // reported panics.
76        panic_info: Vec<(usize, ThreadType, WorkerPanicInfo)>,
77    },
78    /// The storage directory supplied does not match the runtime circuit.
79    IncompatibleStorage,
80    /// Error deserializing checkpointed state.
81    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
125// Thread-local variables set for all threads in the DBSP runtime (foreground and background).
126thread_local! {
127    /// Reference to the `Runtime` that manages this worker thread or `None`
128    /// if the current thread is not running in a multithreaded runtime.
129    static RUNTIME: RefCell<Option<Runtime>> = const { RefCell::new(None) };
130
131    /// `None` means that this is an auxiliary thread that runs inside the runtime
132    /// but is neither a DBSP foreground nor a background thread.
133    static CURRENT_THREAD_TYPE: Cell<Option<ThreadType>> = const { Cell::new(None) };
134
135}
136
137// Thread-local variables set for all foreground worker threads.
138thread_local! {
139    /// 0-based index of the current worker thread within its runtime.
140    /// Returns `0` if the current thread in not running in a multithreaded
141    /// runtime.
142    static WORKER_INDEX: Cell<usize> = const { Cell::new(0) };
143
144    /// Buffer cache for this foreground worker thread.
145    static BUFFER_CACHE: RefCell<Option<Arc<BufferCache>>> = const { RefCell::new(None) };
146}
147
148// Task-local variables set for all tasks in the tokio merger runtime.
149tokio::task_local! {
150    /// The WORKER_INDEX of the FOREGROUND worker thread that this task is doing compaction for.
151    ///
152    /// Set for tasks in the tokio merger runtime.
153    pub(crate) static TOKIO_WORKER_INDEX: usize;
154
155    /// The buffer cache for this task (this cache is shared with the foreground worker thread).
156    ///
157    /// Set for tasks in the tokio merger runtime.
158    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
171/// Local data store shared by all workers in a runtime.
172pub type LocalStore = TypedDashMap<LocalStoreMarker>;
173
174// Rust source code location of a panic.
175#[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/// Information about a panic in a worker thread.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct WorkerPanicInfo {
201    // Panic message, if any.
202    message: Option<String>,
203    // Panic location.
204    location: Option<PanicLocation>,
205    // Backtrace.
206    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    /// Runner configuration.
252    pub config: StorageConfig,
253
254    /// User options.
255    pub options: StorageOptions,
256
257    /// Backend.
258    #[debug(skip)]
259    pub backend: Arc<dyn StorageBackend>,
260
261    // This is just here for the `Drop` behavior.
262    #[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    /// User-configured process memory limit.
273    max_rss: Option<u64>,
274
275    /// Current process RSS.
276    process_rss: AtomicU64,
277
278    /// Current memory pressure updated every second as a function of process_rss, max_rss,
279    /// and previous memory pressure.
280    memory_pressure: AtomicU8,
281
282    /// Monotonic counter incremented whenever memory pressure transitions to high/critical.
283    memory_pressure_epoch: AtomicU64,
284
285    /// Used to notify merger threads when memory pressure changes.
286    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    /// Core IDs to pin foreground workers to.
296    pin_cpus_fg: Vec<CoreId>,
297
298    /// Core IDs to pin tokio merger background workers to.
299    pin_cpus_bg: Vec<CoreId>,
300    fbuf_slab_allocators: Vec<EnumMap<ThreadType, Arc<FBufSlabs>>>,
301    worker_sequence_numbers: Vec<AtomicUsize>,
302    /// Panic info collected from failed worker threads.
303    panic_info: Vec<EnumMap<ThreadType, RwLock<Option<WorkerPanicInfo>>>>,
304    panicked: AtomicBool,
305
306    /// Tokio runtime that runs async merger tasks (see `AsyncMerger`).
307    tokio_merger_runtime: Mutex<Option<TokioRuntime>>,
308
309    /// Optional socket for exchange to listen on.
310    ///
311    /// When exchange initializes, it takes this socket.
312    ///
313    /// Passing in a socket allows the client to let the kernel pick a port
314    /// number by binding to port 0.  Port numbers need to be known for all the
315    /// hosts before we start the circuit, so without this feature we couldn't
316    /// be sure that we'd have a set of available ports.
317    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
342// Map CPU IDs to core IDs for foreground and background workers.
343//
344// Returns a pair of vectors of core IDs, one for foreground workers and one for background workers.
345fn 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            // Dummy buffer cache.
452            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    /// Compute memory pressure based on the current process RSS.
559    ///
560    /// Final memory pressure is computed taking into account the previous memory pressure level as well.
561    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
584// Panic callback used to record worker thread panic information
585// in the runtime.
586//
587// Note: this is a global hook shared by all threads in the process.
588// It is installed when a new DBSP runtime starts, possibly overriding
589// a hook installed by another DBSP instance.  However it should work
590// correctly for threads from different DBSP runtimes or for threads
591// that don't belong to any DBSP runtime since it uses `RUNTIME`
592// thread-local variable to detect a DBSP runtime.
593fn panic_hook(panic_info: &PanicHookInfo<'_>, default_panic_hook: &dyn Fn(&PanicHookInfo<'_>)) {
594    // Call the default panic hook first.
595    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/// A multithreaded runtime that hosts `N` circuits running in parallel worker
607/// threads. Typically, all `N` circuits are identical, but this is not required
608/// or enforced.
609#[repr(transparent)]
610#[derive(Clone, Debug)]
611pub struct Runtime(Arc<RuntimeInner>);
612
613/// A weak reference to a [Runtime].
614#[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/// Stores the default Rust panic hook, so we can invoke it as part of
625/// the DBSP custom hook.
626#[allow(clippy::type_complexity)]
627static DEFAULT_PANIC_HOOK: Lazy<Box<dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send>> =
628    Lazy::new(|| {
629        // Clear any hooks installed by other libraries.
630        let _ = panic::take_hook();
631        panic::take_hook()
632    });
633
634/// Returns the default Rust panic hook.
635fn default_panic_hook() -> &'static (dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send) {
636    &*DEFAULT_PANIC_HOOK
637}
638
639impl Runtime {
640    /// Creates a new runtime with the specified `layout` and runs user-provided
641    /// closure `circuit` in each thread, and returns a handle to the runtime. The closure
642    /// takes an unparker.  The runtime will use this unparker to wake up the thread
643    /// when terminating the circuit.
644    ///
645    /// The `layout` may be specified as a number of worker threads or as a
646    /// [`Layout`].
647    ///
648    /// # Examples
649    /// ```
650    /// # #[cfg(all(windows, miri))]
651    /// # fn main() {}
652    ///
653    /// # #[cfg(not(all(windows, miri)))]
654    /// # fn main() {
655    /// use dbsp::circuit::{Circuit, RootCircuit, Runtime};
656    ///
657    /// // Create a runtime with 4 worker threads.
658    /// let hruntime = Runtime::run(4, |_parker| {
659    ///     // This closure runs within each worker thread.
660    ///     let root = RootCircuit::build(move |circuit| {
661    ///         // Populate `circuit` with operators.
662    ///         Ok(())
663    ///     })
664    ///     .unwrap()
665    ///     .0;
666    ///
667    ///     // Run circuit for 100 clock cycles.
668    ///     for _ in 0..100 {
669    ///         root.transaction().unwrap();
670    ///     }
671    /// })
672    /// .unwrap();
673    ///
674    /// // Wait for all worker threads to terminate.
675    /// hruntime.join().unwrap();
676    /// # }
677    /// ```
678    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        // Install custom panic hook.
693        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        // Create a tokio runtime for async merger tasks.
699        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        // Monitor process RSS and update memory pressure.
729        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                // Update effective memory pressure based on the previous and current memory pressure levels.
744                //
745                // Current memory pressure is computed based on the current process RSS.
746                // Effective memory pressure determines how aggressively the circuit pushes batches to disk.
747                // We introduce a delay between the current memory pressure decreases past the threshold of the
748                // current level, and the effective memory pressure follows.
749                //
750                // - When memory pressure is low or medium, the effective memory pressure follows the current memory pressure.
751                // - When memory pressure is high or critical, we keep the effective memory pressure at the previous level until
752                //   the current memory pressure goes two levels down: high -> low or critical -> moderate.
753                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                // At high and critical levels, wakeup merger threads to push all in-memory batches to disk.
781                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                        // Set the worker's runtime handle and index
809                        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 the worker's circuit
816                        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    /// Returns a reference to the multithreaded runtime that
833    /// manages the current worker thread, or `None` if the thread
834    /// runs without a runtime.
835    ///
836    /// Worker threads created by the [`Runtime::run`] method can access
837    /// the services provided by this API via an instance of `struct Runtime`,
838    /// which they can obtain by calling `Runtime::runtime()`.  DBSP circuits
839    /// created without a managed runtime run in the context of the client
840    /// thread.  When invoked by such a thread, this method returns `None`.
841    #[allow(clippy::self_named_constructors)]
842    pub fn runtime() -> Option<Runtime> {
843        RUNTIME.with(|rt| rt.borrow().clone())
844    }
845
846    /// Returns this runtime's storage backend, if storage is configured.
847    ///
848    /// # Panic
849    ///
850    /// Panics if this thread is not in a [Runtime].
851    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    /// Returns this thread's buffer-cache handle:
863    ///
864    /// - If the thread is a foreground thread or a background task, returns
865    ///   that thread or task's buffer cache.
866    ///
867    /// - If the thread's [Runtime] does not have storage configured, the cache
868    ///   size is trivially small.
869    ///
870    /// - If the thread is not in a [Runtime], then the cache is shared among
871    ///   all such threads. (Such a thread might be in a circuit that uses
872    ///   storage, but there's no way to know because only [Runtime] makes that
873    ///   available at a thread level.)
874    ///
875    /// - If the thread is a background thread, but no buffer cache is set, this
876    ///   ordinarily indicates a bug.  However, this can happen when a
877    ///   background task is dropped, because it's normal for task-local
878    ///   variables to be unavailable in `Drop` during task shutdown.  The
879    ///   function returns `None` in this case (and only in this case).
880    pub fn buffer_cache() -> Option<Arc<BufferCache>> {
881        if let Some(buffer_cache) = BUFFER_CACHE.with(|bc| bc.borrow().clone()) {
882            // Fast path common case for foreground threads.
883            Some(buffer_cache)
884        } else if let Ok(buffer_cache) = TOKIO_BUFFER_CACHE.try_get() {
885            // Background tasks case.
886            Some(buffer_cache)
887        } else if let Some(rt) = Runtime::runtime()
888            && let Some(thread_type) = current_thread_type()
889        {
890            // Slow path for threads running in a [Runtime].
891            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                    // We are in a background thread in a Tokio runtime but no
900                    // buffer cache is set in [TOKIO_BUFFER_CACHE].  This is
901                    // probably a bug, but it can happen when a background task
902                    // is canceled.  It is the reason that this function returns
903                    // an `Option`.
904                    None
905                }
906            }
907        } else {
908            // Fallback path for threads outside a [Runtime].
909            //
910            // This cache is shared by all auxiliary threads in the runtime.  In
911            // particular, output connector threads use it to maintain their
912            // output buffers.
913            //
914            // FIXME: We may need a tunable strategy for aux threads. We cannot
915            // simply give each of them the same cache as DBSP worker threads,
916            // as there can be dozens of aux threads (currently one per output
917            // connector), which do not necessarily need a large cache. OTOH,
918            // sharing the same cache across all of them may potentially cause
919            // performance issues.
920            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    /// Spawn an auxiliary thread inside the runtime.
930    ///
931    /// The auxiliary thread will have access to the runtime's resources, including the
932    /// storage backend. The current use case for this is to be able to use spines outside
933    /// of the DBSP worker threads, e.g., to maintain output buffers.
934    ///
935    /// # Arguments
936    ///
937    /// * `thread_name` - The name of the thread.
938    /// * `parker` - The thread will use this parker when waiting for work. Use it to unpark
939    ///   the thread when terminating the runtime.
940    /// * `f` - The function to execute in the thread.
941    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    /// Returns this runtime's buffer-cache handle for thread type `thread_type`
963    /// in worker with local offset `local_worker_offset`.
964    ///
965    /// Usually it's easier and faster to call [Runtime::buffer_cache] instead.
966    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    /// Returns `(current, max)`, reporting the amount of the buffer cache
983    /// that is currently used and its maximum size, both in bytes.
984    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    /// Returns aggregate statistics for the `FBuf` slab pools used by storage.
1002    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    /// Returns 0-based index of the current worker thread within its runtime.
1021    /// For threads that run without a runtime, this method returns `0`.  In a
1022    /// multihost runtime, this is a global index across all hosts.
1023    ///
1024    /// For threads that run in the tokio merger runtime, this method returns the
1025    /// index of the foreground worker thread that this task is doing compaction for.
1026    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                    // We are running in an auxiliary thread.  Treat it like the
1033                    // first thread in the local runtime.
1034                    runtime.layout().local_workers().start
1035                }
1036                None => {
1037                    // We are not running in a runtime.
1038                    0
1039                }
1040            }),
1041        }
1042    }
1043
1044    /// Returns the 0-based index of the current worker within its local host.
1045    pub fn local_worker_offset() -> usize {
1046        // Find the lowest-numbered local worker.
1047        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    /// Returns the worker index as a string.
1078    ///
1079    /// This is useful for metric labels.
1080    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    /// Returns the minimum number of bytes in a batch produced by a merge operator to spill it to storage.
1118    ///
1119    /// The output is determined by the current memory pressure level and the user-configured `min_storage_bytes` option.
1120    /// When memory pressure is low, the output is `min_storage_bytes`; when memory pressure is moderate, high or critical,
1121    /// the output is `0`, meaning all batches are spilled to storage.
1122    ///
1123    /// # Returns
1124    ///
1125    /// - `None` - if this thread doesn't have a Runtime or if it doesn't have storage configured.
1126    /// - `Some(0)` - spill all batches to storage.
1127    /// - `Some(N)` - spill batches with size >= N to storage.
1128    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                    // This reduces the files stored on disk to a reasonable number.
1139
1140                    10 * 1024 * 1024
1141                }))
1142            }
1143        })
1144    }
1145
1146    /// Returns the minimum number of bytes in a batch inserted into a spine by a foreground worker to spill it to storage.
1147    ///
1148    /// The output is determined by the current memory pressure level and the user-configured `min_storage_bytes` option.
1149    /// When memory pressure is low the output is `usize::MAX`, when memory pressure is moderate, the output is `min_storage_bytes`,
1150    /// when memory pressure is high or critical, the output is `0`, meaning all batches are spilled to storage.
1151    ///
1152    /// # Returns
1153    ///
1154    /// - `None` - if this thread doesn't have a Runtime or if it doesn't have storage configured.
1155    /// - `Some(0)` - spill all batches to storage.
1156    /// - `Some(N)` - spill batches with size >= N to storage.
1157    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                // Moderate pressure: spill large batches to storage in the foreground; the merger will take care of the rest.
1167                Some(
1168                    storage
1169                        .options
1170                        .min_storage_bytes
1171                        .unwrap_or(10 * 1024 * 1024),
1172                )
1173            } else {
1174                // When there is no memory pressure, we leave it to the merger to write the batches to storage
1175                // eventually.
1176                Some(usize::MAX)
1177            }
1178        })
1179    }
1180
1181    /// Returns the minimum number of bytes in a transient batch exchanged between DBSP operators during a step of the
1182    /// circuit to spill it to storage.
1183    ///
1184    /// The output is determined by the current memory pressure level and the user-configured `min_step_storage_bytes` option.
1185    /// When memory pressure is below critical, the output is `min_step_storage_bytes`, when memory pressure is critical,
1186    /// the output is `0`, meaning all batches are spilled to storage.
1187    ///
1188    /// # Returns
1189    ///
1190    /// - `None` - if this thread doesn't have a Runtime or if it doesn't have storage configured.
1191    /// - `Some(0)` - spill all batches to storage.
1192    /// - `Some(N)` - spill batches with size >= N to storage.
1193    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    /// Returns the number of workers in the runtime's [`Layout`].  In a
1228    /// multihost runtime, this is the total number of workers across all hosts.
1229    ///
1230    /// If this thread is not in a [Runtime], returns 1.
1231    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    /// Returns the [`Layout`] for this runtime.
1240    pub fn layout(&self) -> &Layout {
1241        &self.inner().layout
1242    }
1243
1244    /// Returns reference to the data store shared by all workers within the
1245    /// runtime.  In a multihost runtime, this data store is local to this
1246    /// particular host.
1247    ///
1248    /// This low-level mechanism can be used by various services that
1249    /// require common state shared across all workers on a host.
1250    ///
1251    /// The [`LocalStore`] type is an alias to [`TypedDashMap`], a
1252    /// concurrent map type that can store key/value pairs of different
1253    /// types.  See `typedmap` crate documentation for details.
1254    pub fn local_store(&self) -> &LocalStore {
1255        &self.inner().store
1256    }
1257
1258    /// Returns the path to the storage directory for this runtime.
1259    pub fn storage_path(&self) -> Option<&Path> {
1260        self.inner()
1261            .storage
1262            .as_ref()
1263            .map(|storage| storage.config.path())
1264    }
1265
1266    /// A per-worker sequential counter.
1267    ///
1268    /// This method can be used to generate unique identifiers that will be the
1269    /// same across all worker threads.  Repeated calls to this function
1270    /// from the same worker generate numbers 0, 1, 2, ...
1271    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    /// `true` if the current worker thread has received a kill signal
1277    /// and should exit asap.  Schedulers should use this method before
1278    /// scheduling the next operator and after parking.
1279    pub fn kill_in_progress() -> bool {
1280        // Only a circuit with a `Runtime` can receive a kill signal, which is
1281        // OK because a kill request can only be sent via a `RuntimeHandle`
1282        // anyway.
1283        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    // Record information about a worker thread panic in `panic_info`
1310    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            // We only install panic hooks on foreground and background threads,
1314            // so this shouldn't happen, but we cannot panic here.
1315            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    /// Handle to the tokio merger runtime associated with this DBSP runtime.
1326    ///
1327    /// Returns `None` if the runtime has already been torn down. Callers may
1328    /// race `RuntimeHandle::join`, which takes the runtime out of its slot
1329    /// before dropping it; an in-flight merger task that reaches this accessor
1330    /// after the take observes `None` and should bail out of its work.
1331    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    /// Takes and returns the TCP listener socket configured via
1341    /// [CircuitConfig], if any.
1342    ///
1343    /// [CircuitConfig]: crate::circuit::dbsp_handle::CircuitConfig
1344    pub(crate) fn take_exchange_listener(&self) -> Option<TcpListener> {
1345        self.inner().exchange_listener.lock().unwrap().take()
1346    }
1347}
1348
1349/// A synchronization primitive that allows multiple threads within a runtime to agree
1350/// when a condition is satisfied.
1351pub(crate) struct Consensus(Broadcast<bool>);
1352
1353impl Consensus {
1354    pub fn new(name: impl Display) -> Self {
1355        Self(Broadcast::new(name))
1356    }
1357
1358    /// Returns `true` if all workers vote `true`.
1359    ///
1360    /// # Arguments
1361    ///
1362    /// * `local` - Local vote by the current worker.
1363    pub async fn check(&self, local: bool) -> Result<bool, SchedulerError> {
1364        Ok(self.0.collect(local).await?.into_iter().all(identity))
1365    }
1366}
1367
1368/// A synchronization primitive that allows multiple threads within a runtime to broadcast
1369/// a value to all other workers.
1370pub(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    /// Returns values broadcast by all workers (including the current worker), indexed by worker id.
1399    ///
1400    /// # Arguments
1401    ///
1402    /// * `local` - Value broadcast by the current worker.
1403    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                // This span shows elapsed time from sending our broadcast to
1411                // getting all the others' results back.
1412                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/// Handle returned by `Runtime::run`.
1440#[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    /// Unpark worker thread.
1452    ///
1453    /// Workers release the CPU by parking when they have no work to do.
1454    /// This method unparks a thread after sending a command to it or
1455    /// when killing a circuit.
1456    pub(super) fn unpark_worker(&self, worker: usize) {
1457        self.workers[worker].1.unpark();
1458    }
1459
1460    /// Returns reference to the runtime.
1461    pub fn runtime(&self) -> &Runtime {
1462        &self.runtime
1463    }
1464
1465    /// Terminate the runtime and all worker threads without waiting for any
1466    /// in-progress computation to complete.
1467    ///
1468    /// Signals all workers to exit.  Any operators already running are
1469    /// evaluated to completion, after which the worker thread terminates
1470    /// even if the circuit has not been fully evaluated for the current
1471    /// clock cycle.
1472    pub fn kill(self) -> ThreadResult<()> {
1473        self.kill_async();
1474        self.join()
1475    }
1476
1477    // Signals all worker threads to exit, and returns immediately without
1478    // waiting for them to exit.
1479    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    /// Wait for all workers in the runtime to terminate.
1501    ///
1502    /// The calling thread blocks until all worker threads have terminated.
1503    pub fn join(self) -> ThreadResult<()> {
1504        // Insist on joining all threads even if some of them fail.
1505        #[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        // Once all fg threads have terminated, signal the aux threads to terminate as well.
1513        // Normally this is not needed, since this function is usually called from `kill_async`,
1514        // which already signals the aux threads to terminate, but it is useful when it is called
1515        // directly, e.g., in some tests.
1516        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        // Wait for aux threads.
1532        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        // Terminate the tokio merger runtime.
1543        //
1544        // The `MutexGuard` returned by `lock()` must be dropped before the
1545        // tokio runtime: dropping the runtime blocks until every blocking-pool
1546        // task yields, and those tasks may call `tokio_merger_runtime()`, which
1547        // re-locks this same mutex. Holding the guard across the runtime drop
1548        // would deadlock. Binding `take()` to a `let` releases the guard at the
1549        // `;`, leaving an owned `Option<TokioRuntime>` whose drop runs without
1550        // the mutex held.
1551        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        // This must happen after we wait for the background threads, because
1561        // they might try to initiate another merge before they exit, which
1562        // would require them to have access to storage, which is kept in the
1563        // local store.
1564        self.runtime.local_store().clear();
1565
1566        results.into_iter().collect()
1567    }
1568
1569    /// Retrieve panic info for a specific worker.
1570    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    /// Retrieve panic info for all workers.
1579    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    /// Returns true if any worker has panicked.
1593    pub fn panicked(&self) -> bool {
1594        self.runtime.inner().panicked.load(Ordering::Acquire)
1595    }
1596}
1597
1598/// Where a worker thread is.
1599#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1600pub enum WorkerLocation {
1601    /// In this process.
1602    Local,
1603    /// Across the network.
1604    Remote,
1605}
1606
1607/// An iterator for all of the workers in a runtime.
1608///
1609/// For every worker in a [Runtime], this iterator yields its [WorkerLocation].
1610#[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    /// Constructs a new iterator for all the worker locations in the current
1624    /// [Runtime].  Use [Iterator::enumerate] to also obtain the associated
1625    /// worker indexes.
1626    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    /// Constructs a new iterator from `layout`.
1638    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    /// Returns the location of the worker with the given index.
1646    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    // Serialize all tests that use MOCK_PROCESS_RSS_BYTES so they don't race with each other.
1720    static MOCK_RSS_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1721
1722    fn set_mock_process_rss_bytes(bytes: u64) {
1723        // Safety: We assume that only DBSP code accesses the environment variables.
1724        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            // Safety: We assume that only DBSP code accesses the environment variables.
1734            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        // Case 1: storage specified, runtime should not clean up storage when exiting
1806        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                // Generator that produces values using `sequence_next`.
2096                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            // The scheduler allocates the first value for metadata exchange; therefore the output starts from 2.
2109            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    // Test `RuntimeHandle::kill`.
2122    fn test_kill<S>()
2123    where
2124        S: Scheduler + 'static,
2125    {
2126        let hruntime = Runtime::run(16, |_parker| {
2127            // Create a nested circuit that iterates forever.
2128            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 the memory pressure thresholds and how merger threads behave under different memory pressure levels.
2159    #[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        // Create a circuit with a single spine.
2175        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        // Push 7 batches. This is below the merge threshold, so no batches should get merged,
2183        // and al of them should live in memory.
2184        for key in 0..7 {
2185            input_handle.push(key, 1);
2186            circuit.transaction().unwrap();
2187        }
2188
2189        // Baseline (mock RSS = 0): no pressure and default spill thresholds.
2190        let (pressure, min_merge, min_insert, min_step) =
2191            query_runtime_memory_state(circuit.runtime());
2192        assert_eq!(pressure, MemoryPressure::Low);
2193
2194        // Verify the spill thresholds: at low pressure, merge threshold is set to the user-configured `min_storage_bytes`,
2195        // insert threshold is set to `usize::MAX`, and step threshold is set to `usize::MAX`.
2196        assert_eq!(min_merge, TEN_MIB);
2197        assert_eq!(min_insert, usize::MAX);
2198        assert_eq!(min_step, usize::MAX);
2199
2200        // Verify the spine currently holds in-memory tuples before pressure rises.
2201        let profile = circuit.retrieve_profile().unwrap();
2202        assert!(in_memory_spine_records(&profile) == 7);
2203
2204        // Moderate pressure (8.8/10 GiB): merge threshold drops to 0, insert/step stay unchanged.
2205        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        // High pressure (9.4/10 GiB): insert threshold drops to 0 as well.
2216        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        // Under high pressure, merges should eventually spill all in-memory tuples to storage.
2227        // Poll profile until this converges to 0.
2228        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        // Critical pressure (9.8/10 GiB): all spill thresholds must be 0.
2242        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}