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, ExchangeActivity};
11use crate::storage::backend::StorageBackend;
12use crate::storage::file::format::Compression;
13use crate::storage::file::writer::Parameters;
14use crate::utils::process_rss_bytes;
15use crate::{
16    DetailedError,
17    storage::{
18        backend::StorageError,
19        buffer_cache::{BufferCache, build_buffer_caches},
20        dirlock::LockedDirectory,
21    },
22};
23use core_affinity::{CoreId, get_core_ids};
24use crossbeam::sync::{Parker, Unparker};
25use enum_map::{Enum, EnumMap, enum_map};
26use feldera_buffer_cache::ThreadType;
27use feldera_samply::{LongSpanBuilder, Span};
28use feldera_storage::fbuf::FBuf;
29use feldera_storage::fbuf::slab::{FBufSlabs, FBufSlabsStats, set_thread_slab_pool};
30use feldera_types::config::{DevTweaks, StorageCompression, StorageConfig, StorageOptions};
31use feldera_types::memory_pressure::{
32    CRITICAL_MEMORY_PRESSURE_THRESHOLD, HIGH_MEMORY_PRESSURE_THRESHOLD,
33    MODERATE_MEMORY_PRESSURE_THRESHOLD, MemoryPressure,
34};
35use indexmap::IndexSet;
36use once_cell::sync::Lazy;
37use serde::Serialize;
38use std::convert::identity;
39use std::iter::repeat;
40use std::net::TcpListener;
41use std::ops::{Index, Range};
42use std::path::Path;
43use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize};
44use std::sync::{LazyLock, Mutex};
45use std::time::Duration;
46use std::{
47    backtrace::Backtrace,
48    borrow::Cow,
49    cell::{Cell, RefCell},
50    collections::HashSet,
51    error::Error as StdError,
52    fmt,
53    fmt::{Debug, Display, Error as FmtError, Formatter},
54    panic::{self, Location, PanicHookInfo},
55    sync::{
56        Arc, RwLock, Weak,
57        atomic::{AtomicBool, Ordering},
58    },
59    thread::{Builder, JoinHandle, Result as ThreadResult},
60};
61use tokio::runtime::Builder as TokioBuilder;
62use tokio::runtime::Runtime as TokioRuntime;
63use tokio::sync::Notify;
64use tokio_util::sync::CancellationToken;
65use tracing::{debug, error, info, warn};
66use typedmap::TypedDashMap;
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
69pub enum Error {
70    /// 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    /// A bootstrap-circuit command was issued in the wrong state, e.g.,
83    /// stepping a bootstrap circuit that does not exist or creating one
84    /// while another exists.
85    BootstrapCircuit(String),
86    Terminated,
87}
88
89impl DetailedError for Error {
90    fn error_code(&self) -> Cow<'static, str> {
91        match self {
92            Self::UnknownPersistentId(_) => Cow::from("UnknownPersistentId"),
93            Self::WorkerPanic { .. } => Cow::from("WorkerPanic"),
94            Self::Terminated => Cow::from("Terminated"),
95            Self::IncompatibleStorage => Cow::from("IncompatibleStorage"),
96            Self::CheckpointParseError(_) => Cow::from("CheckpointParseError"),
97            Self::BootstrapCircuit(_) => Cow::from("BootstrapCircuit"),
98        }
99    }
100}
101
102impl Display for Error {
103    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
104        match self {
105            Self::UnknownPersistentId(persistent_id) => {
106                write!(f, "Unknown persistent node id: {persistent_id}")
107            }
108            Self::WorkerPanic { panic_info } => {
109                writeln!(f, "One or more worker threads terminated unexpectedly")?;
110
111                for (worker, thread_type, worker_panic_info) in panic_info.iter() {
112                    writeln!(f, "{thread_type} worker thread {worker} panicked")?;
113                    writeln!(f, "{worker_panic_info}")?;
114                }
115                Ok(())
116            }
117            Self::Terminated => f.write_str("circuit has been terminated"),
118            Self::IncompatibleStorage => {
119                f.write_str("Supplied storage directory does not fit the runtime circuit")
120            }
121            Self::CheckpointParseError(error) => {
122                write!(f, "Error deserializing checkpointed state: {error}")
123            }
124            Self::BootstrapCircuit(error) => {
125                write!(f, "Bootstrap circuit error: {error}")
126            }
127        }
128    }
129}
130
131impl StdError for Error {}
132
133// Thread-local variables set for all threads in the DBSP runtime (foreground and background).
134thread_local! {
135    /// Reference to the `Runtime` that manages this worker thread or `None`
136    /// if the current thread is not running in a multithreaded runtime.
137    static RUNTIME: RefCell<Option<Runtime>> = const { RefCell::new(None) };
138
139    /// `None` means that this is an auxiliary thread that runs inside the runtime
140    /// but is neither a DBSP foreground nor a background thread.
141    static CURRENT_THREAD_TYPE: Cell<Option<ThreadType>> = const { Cell::new(None) };
142
143}
144
145// Thread-local variables set for all foreground worker threads.
146thread_local! {
147    /// 0-based index of the current worker thread within its runtime.
148    /// Returns `0` if the current thread in not running in a multithreaded
149    /// runtime.
150    static WORKER_INDEX: Cell<usize> = const { Cell::new(0) };
151
152    /// Buffer cache for this foreground worker thread.
153    static BUFFER_CACHE: RefCell<Option<Arc<BufferCache>>> = const { RefCell::new(None) };
154}
155
156// Task-local variables set for all tasks in the tokio merger runtime.
157tokio::task_local! {
158    /// The WORKER_INDEX of the FOREGROUND worker thread that this task is doing compaction for.
159    ///
160    /// Set for tasks in the tokio merger runtime.
161    pub(crate) static TOKIO_WORKER_INDEX: usize;
162
163    /// The buffer cache for this task (this cache is shared with the foreground worker thread).
164    ///
165    /// Set for tasks in the tokio merger runtime.
166    pub(crate) static TOKIO_BUFFER_CACHE: Arc<BufferCache>;
167}
168
169pub(crate) fn current_thread_type() -> Option<ThreadType> {
170    CURRENT_THREAD_TYPE.get()
171}
172
173fn set_current_thread_type(thread_type: ThreadType) {
174    CURRENT_THREAD_TYPE.set(Some(thread_type));
175}
176
177pub struct LocalStoreMarker;
178
179/// Local data store shared by all workers in a runtime.
180pub type LocalStore = TypedDashMap<LocalStoreMarker>;
181
182// Rust source code location of a panic.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184pub struct PanicLocation {
185    file: String,
186    line: u32,
187    col: u32,
188}
189
190impl Display for PanicLocation {
191    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
192        write!(f, "{}:{}:{}", self.file, self.line, self.col)
193    }
194}
195
196impl PanicLocation {
197    fn new(loc: &Location) -> Self {
198        Self {
199            file: loc.file().to_string(),
200            line: loc.line(),
201            col: loc.column(),
202        }
203    }
204}
205
206/// Information about a panic in a worker thread.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
208pub struct WorkerPanicInfo {
209    // Panic message, if any.
210    message: Option<String>,
211    // Panic location.
212    location: Option<PanicLocation>,
213    // Backtrace.
214    backtrace: String,
215}
216
217impl Display for WorkerPanicInfo {
218    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
219        if let Some(message) = &self.message {
220            writeln!(f, "panic message: {message}")?;
221        } else {
222            writeln!(f, "panic message (none)")?;
223        }
224
225        if let Some(location) = &self.location {
226            writeln!(f, "panic location: {location}")?;
227        } else {
228            writeln!(f, "panic location: unknown")?;
229        }
230        writeln!(f, "stack trace:\n{}", self.backtrace)
231    }
232}
233
234impl WorkerPanicInfo {
235    fn new(panic_info: &PanicHookInfo) -> Self {
236        #[allow(clippy::manual_map)]
237        let message = if let Some(v) = panic_info.payload().downcast_ref::<String>() {
238            Some(v.clone())
239        } else if let Some(v) = panic_info.payload().downcast_ref::<&str>() {
240            Some(v.to_string())
241        } else {
242            None
243        };
244        let backtrace = Backtrace::force_capture().to_string();
245        let location = panic_info
246            .location()
247            .map(|location| PanicLocation::new(location));
248
249        Self {
250            message,
251            location,
252            backtrace,
253        }
254    }
255}
256
257#[derive(derive_more::Debug)]
258struct RuntimeStorage {
259    /// Runner configuration.
260    pub config: StorageConfig,
261
262    /// User options.
263    pub options: StorageOptions,
264
265    /// Backend.
266    #[debug(skip)]
267    pub backend: Arc<dyn StorageBackend>,
268
269    // This is just here for the `Drop` behavior.
270    #[allow(dead_code)]
271    locked_directory: LockedDirectory,
272}
273
274struct RuntimeInner {
275    layout: Layout,
276    mode: Mode,
277    step_size: StepSize,
278    allow_input_during_commit: bool,
279    dev_tweaks: DevTweaks,
280
281    /// User-configured process memory limit.
282    max_rss: Option<u64>,
283
284    /// Current process RSS.
285    process_rss: AtomicU64,
286
287    /// Current memory pressure updated every second as a function of process_rss, max_rss,
288    /// and previous memory pressure.
289    memory_pressure: AtomicU8,
290
291    /// Monotonic counter incremented whenever memory pressure transitions to high/critical.
292    memory_pressure_epoch: AtomicU64,
293
294    /// Used to notify merger threads when memory pressure changes.
295    memory_pressure_notify: Arc<Notify>,
296
297    storage: Option<RuntimeStorage>,
298    store: LocalStore,
299    kill_signal: AtomicBool,
300    cancellation_token: CancellationToken,
301    aux_threads: Mutex<Vec<(JoinHandle<()>, Unparker)>>,
302    buffer_caches: Vec<EnumMap<ThreadType, Arc<BufferCache>>>,
303
304    /// Core IDs to pin foreground workers to.
305    pin_cpus_fg: Vec<CoreId>,
306
307    /// Core IDs to pin tokio merger background workers to.
308    pin_cpus_bg: Vec<CoreId>,
309    fbuf_slab_allocators: Vec<EnumMap<ThreadType, Arc<FBufSlabs>>>,
310    worker_sequence_numbers: Vec<AtomicUsize>,
311    /// Panic info collected from failed worker threads.
312    panic_info: Vec<EnumMap<ThreadType, RwLock<Option<WorkerPanicInfo>>>>,
313    panicked: AtomicBool,
314
315    /// Tokio runtime that runs async merger tasks (see `AsyncMerger`).
316    tokio_merger_runtime: Mutex<Option<TokioRuntime>>,
317
318    /// Optional socket for exchange to listen on.
319    ///
320    /// When exchange initializes, it takes this socket.
321    ///
322    /// Passing in a socket allows the client to let the kernel pick a port
323    /// number by binding to port 0.  Port numbers need to be known for all the
324    /// hosts before we start the circuit, so without this feature we couldn't
325    /// be sure that we'd have a set of available ports.
326    exchange_listener: Mutex<Option<TcpListener>>,
327}
328
329impl Drop for RuntimeInner {
330    fn drop(&mut self) {
331        debug!("dropping RuntimeInner");
332    }
333}
334
335impl Debug for RuntimeInner {
336    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
337        f.debug_struct("RuntimeInner")
338            .field("layout", &self.layout)
339            .field("storage", &self.storage)
340            .finish()
341    }
342}
343
344fn display_core_ids<'a>(iter: impl Iterator<Item = &'a CoreId>) -> String {
345    format!(
346        "{:?}",
347        iter.map(|core| core.id).collect::<Vec<_>>().as_slice()
348    )
349}
350
351// Map CPU IDs to core IDs for foreground and background workers.
352//
353// Returns a pair of vectors of core IDs, one for foreground workers and one for background workers.
354fn map_pin_cpus(config: &CircuitConfig) -> (Vec<CoreId>, Vec<CoreId>) {
355    if config.layout.is_multihost() {
356        if !config.pin_cpus.is_empty() {
357            warn!("CPU pinning not yet supported with multihost DBSP");
358        }
359        return (Vec::new(), Vec::new());
360    }
361
362    let merger_threads = config.num_merger_threads();
363
364    let nworkers = config.layout.n_workers();
365    let pin_cpus = config
366        .pin_cpus
367        .iter()
368        .copied()
369        .map(|id| CoreId { id })
370        .collect::<IndexSet<_>>();
371    let expected_cpus = nworkers + merger_threads;
372    if pin_cpus.len() < expected_cpus {
373        if !pin_cpus.is_empty() {
374            warn!(
375                "ignoring CPU pinning request because {nworkers} foreground workers and {} merger workers require {} pinned CPUs but only {} were specified",
376                merger_threads,
377                expected_cpus,
378                pin_cpus.len()
379            )
380        }
381        return (Vec::new(), Vec::new());
382    }
383
384    let Some(core_ids) = get_core_ids() else {
385        warn!(
386            "ignoring CPU pinning request because this system's core ids list could not be obtained"
387        );
388        return (Vec::new(), Vec::new());
389    };
390    let core_ids = core_ids.iter().copied().collect::<IndexSet<_>>();
391
392    let missing_cpus = pin_cpus.difference(&core_ids).copied().collect::<Vec<_>>();
393    if !missing_cpus.is_empty() {
394        warn!(
395            "ignoring CPU pinning request because requested CPUs {missing_cpus:?} are not available (available CPUs are: {})",
396            display_core_ids(core_ids.iter())
397        );
398        return (Vec::new(), Vec::new());
399    }
400
401    let fg_cpus = &pin_cpus[0..nworkers];
402    let bg_cpus = &pin_cpus[nworkers..expected_cpus];
403    info!(
404        "pinning foreground workers to CPUs {} and background workers to CPUs {}",
405        display_core_ids(fg_cpus.iter()),
406        display_core_ids(bg_cpus.iter())
407    );
408    let fg_pinning = (0..nworkers).map(|i| fg_cpus[i]).collect();
409
410    let bg_pinning = (0..merger_threads).map(|i| bg_cpus[i]).collect();
411
412    (fg_pinning, bg_pinning)
413}
414
415impl RuntimeInner {
416    fn new(config: CircuitConfig) -> Result<Self, DbspError> {
417        let nworkers = config.layout.local_workers().len();
418        let buffer_cache_strategy = config.dev_tweaks.buffer_cache_strategy();
419        let buffer_max_buckets = config.dev_tweaks.buffer_max_buckets;
420        let buffer_cache_allocation_strategy = config
421            .dev_tweaks
422            .effective_buffer_cache_allocation_strategy();
423        let fbuf_slab_bytes_per_class = config
424            .dev_tweaks
425            .fbuf_slab_bytes_per_class
426            .unwrap_or(FBufSlabs::DEFAULT_BYTES_PER_CLASS);
427        let storage = if let Some(storage) = config.storage.clone() {
428            let locked_directory =
429                LockedDirectory::new_blocking(storage.config.path(), Duration::from_secs(60))?;
430            let backend = storage.backend;
431
432            if let Some(init_checkpoint) = storage.init_checkpoint
433                && !backend
434                    .exists(&Checkpointer::checkpoint_dir(init_checkpoint).child("CHECKPOINT"))?
435            {
436                return Err(DbspError::Storage(StorageError::CheckpointNotFound(
437                    init_checkpoint,
438                )));
439            }
440
441            Some(RuntimeStorage {
442                config: storage.config,
443                options: storage.options,
444                backend,
445                locked_directory,
446            })
447        } else {
448            None
449        };
450
451        let total_cache_bytes = if let Some(storage) = &storage {
452            match storage.options.cache_mib {
453                Some(cache_mib) => cache_mib.saturating_mul(1024 * 1024),
454                None => 256usize
455                    .saturating_mul(1024 * 1024)
456                    .saturating_mul(nworkers)
457                    .saturating_mul(ThreadType::LENGTH),
458            }
459        } else {
460            // Dummy buffer cache.
461            1
462        };
463
464        info!(
465            "Setting up buffer caches: {buffer_cache_strategy:?} {buffer_cache_allocation_strategy:?} buckets={buffer_max_buckets:?} total_size={:?} MiB",
466            total_cache_bytes / (1024 * 1024)
467        );
468        let buffer_caches = build_buffer_caches(
469            nworkers,
470            total_cache_bytes,
471            buffer_cache_strategy,
472            buffer_max_buckets,
473            buffer_cache_allocation_strategy,
474        );
475
476        info!("Setting up FBuf slab allocators: bytes_per_class={fbuf_slab_bytes_per_class}");
477        let fbuf_slab_allocators = (0..nworkers)
478            .map(|_| {
479                let allocator = Arc::new(FBufSlabs::new(fbuf_slab_bytes_per_class));
480                enum_map! {
481                    ThreadType::Foreground => allocator.clone(),
482                    ThreadType::Background => allocator.clone(),
483                }
484            })
485            .collect();
486
487        let (pin_cpus_fg, pin_cpus_bg) = map_pin_cpus(&config);
488
489        if !config.dev_tweaks.other_options.is_empty() {
490            warn!(
491                "Circuit dev_tweaks includes unknown options: {:?}",
492                &config.dev_tweaks.other_options
493            );
494        }
495
496        if config.dev_tweaks.streaming_exchange() {
497            info!("dev_tweaks.streaming_exchange enabled");
498        } else {
499            info!("dev_tweaks.streaming_exchange disabled");
500        }
501
502        Ok(Self {
503            pin_cpus_fg,
504            pin_cpus_bg,
505            layout: config.layout,
506            mode: config.mode,
507            step_size: config.step_size,
508            allow_input_during_commit: config.allow_input_during_commit,
509            dev_tweaks: config.dev_tweaks,
510            max_rss: config.max_rss_bytes,
511            process_rss: AtomicU64::new(process_rss_bytes().unwrap_or_default()),
512            memory_pressure: AtomicU8::new(MemoryPressure::Low as u8),
513            memory_pressure_epoch: AtomicU64::new(0),
514            memory_pressure_notify: Arc::new(Notify::new()),
515            storage,
516            store: TypedDashMap::new(),
517            kill_signal: AtomicBool::new(false),
518            cancellation_token: CancellationToken::new(),
519            aux_threads: Mutex::new(Vec::new()),
520            buffer_caches,
521            fbuf_slab_allocators,
522            worker_sequence_numbers: (0..nworkers).map(|_| AtomicUsize::new(0)).collect(),
523            panic_info: (0..nworkers)
524                .map(|_| EnumMap::from_fn(|_| RwLock::new(None)))
525                .collect(),
526            panicked: AtomicBool::new(false),
527            tokio_merger_runtime: Mutex::new(None),
528            exchange_listener: Mutex::new(config.exchange_listener),
529        })
530    }
531
532    fn pin_cpu(&self) {
533        match current_thread_type() {
534            Some(ThreadType::Foreground) => {
535                if !self.pin_cpus_fg.is_empty() {
536                    let local_worker_offset = Runtime::local_worker_offset();
537                    let core = self.pin_cpus_fg[local_worker_offset];
538                    if !core_affinity::set_for_current(core) {
539                        warn!(
540                            "failed to pin foreground worker {local_worker_offset} to core {}",
541                            core.id
542                        );
543                    }
544                }
545            }
546            Some(ThreadType::Background) => {
547                if !self.pin_cpus_bg.is_empty() {
548                    let local_worker_offset = Runtime::local_worker_offset();
549                    let core = self.pin_cpus_bg[local_worker_offset];
550                    if !core_affinity::set_for_current(core) {
551                        warn!(
552                            "failed to pin background worker {local_worker_offset} to core {}",
553                            core.id
554                        );
555                    }
556                }
557            }
558            None => {
559                panic!("pin_cpu() called outside of a runtime or on an aux thread");
560            }
561        }
562    }
563
564    fn memory_pressure(&self) -> MemoryPressure {
565        MemoryPressure::try_from(self.memory_pressure.load(Ordering::Relaxed)).unwrap()
566    }
567
568    /// Compute memory pressure based on the current process RSS.
569    ///
570    /// Final memory pressure is computed taking into account the previous memory pressure level as well.
571    fn raw_memory_pressure(&self, process_rss: u64) -> MemoryPressure {
572        let process_rss = process_rss as f64;
573
574        let Some(max_rss) = self.max_rss else {
575            return MemoryPressure::Low;
576        };
577
578        if process_rss >= (CRITICAL_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
579            return MemoryPressure::Critical;
580        }
581
582        if process_rss >= (HIGH_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
583            return MemoryPressure::High;
584        }
585
586        if process_rss >= (MODERATE_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
587            return MemoryPressure::Moderate;
588        }
589
590        MemoryPressure::Low
591    }
592}
593
594// Panic callback used to record worker thread panic information
595// in the runtime.
596//
597// Note: this is a global hook shared by all threads in the process.
598// It is installed when a new DBSP runtime starts, possibly overriding
599// a hook installed by another DBSP instance.  However it should work
600// correctly for threads from different DBSP runtimes or for threads
601// that don't belong to any DBSP runtime since it uses `RUNTIME`
602// thread-local variable to detect a DBSP runtime.
603fn panic_hook(panic_info: &PanicHookInfo<'_>, default_panic_hook: &dyn Fn(&PanicHookInfo<'_>)) {
604    // Call the default panic hook first.
605    default_panic_hook(panic_info);
606
607    RUNTIME.with(|runtime| {
608        if let Ok(runtime) = runtime.try_borrow()
609            && let Some(runtime) = runtime.as_ref()
610        {
611            runtime.panic(panic_info);
612        }
613    })
614}
615
616/// A multithreaded runtime that hosts `N` circuits running in parallel worker
617/// threads. Typically, all `N` circuits are identical, but this is not required
618/// or enforced.
619#[repr(transparent)]
620#[derive(Clone, Debug)]
621pub struct Runtime(Arc<RuntimeInner>);
622
623/// A weak reference to a [Runtime].
624#[repr(transparent)]
625#[derive(Clone, Debug)]
626pub struct WeakRuntime(Weak<RuntimeInner>);
627
628impl WeakRuntime {
629    pub fn upgrade(&self) -> Option<Runtime> {
630        self.0.upgrade().map(Runtime)
631    }
632}
633
634/// Stores the default Rust panic hook, so we can invoke it as part of
635/// the DBSP custom hook.
636#[allow(clippy::type_complexity)]
637static DEFAULT_PANIC_HOOK: Lazy<Box<dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send>> =
638    Lazy::new(|| {
639        // Clear any hooks installed by other libraries.
640        let _ = panic::take_hook();
641        panic::take_hook()
642    });
643
644/// Returns the default Rust panic hook.
645fn default_panic_hook() -> &'static (dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send) {
646    &*DEFAULT_PANIC_HOOK
647}
648
649impl Runtime {
650    /// Creates a new runtime with the specified `layout` and runs user-provided
651    /// closure `circuit` in each thread, and returns a handle to the runtime. The closure
652    /// takes an unparker.  The runtime will use this unparker to wake up the thread
653    /// when terminating the circuit.
654    ///
655    /// The `layout` may be specified as a number of worker threads or as a
656    /// [`Layout`].
657    ///
658    /// # Examples
659    /// ```
660    /// # #[cfg(all(windows, miri))]
661    /// # fn main() {}
662    ///
663    /// # #[cfg(not(all(windows, miri)))]
664    /// # fn main() {
665    /// use dbsp::circuit::{Circuit, RootCircuit, Runtime};
666    ///
667    /// // Create a runtime with 4 worker threads.
668    /// let hruntime = Runtime::run(4, |_parker| {
669    ///     // This closure runs within each worker thread.
670    ///     let root = RootCircuit::build(move |circuit| {
671    ///         // Populate `circuit` with operators.
672    ///         Ok(())
673    ///     })
674    ///     .unwrap()
675    ///     .0;
676    ///
677    ///     // Run circuit for 100 clock cycles.
678    ///     for _ in 0..100 {
679    ///         root.transaction().unwrap();
680    ///     }
681    /// })
682    /// .unwrap();
683    ///
684    /// // Wait for all worker threads to terminate.
685    /// hruntime.join().unwrap();
686    /// # }
687    /// ```
688    pub fn run<F>(config: impl Into<CircuitConfig>, circuit: F) -> Result<RuntimeHandle, DbspError>
689    where
690        F: FnOnce(Parker) + Clone + Send + 'static,
691    {
692        let mut config: CircuitConfig = config.into();
693        if config.step_size == StepSize::FullSteps {
694            config.dev_tweaks.splitter_chunk_size_records = Some(u64::MAX);
695        }
696
697        let workers = config.layout.local_workers();
698        let num_merger_threads = config.num_merger_threads();
699
700        let runtime = Self(Arc::new(RuntimeInner::new(config)?));
701
702        // Install custom panic hook.
703        let default_hook = default_panic_hook();
704        panic::set_hook(Box::new(move |panic_info| {
705            panic_hook(panic_info, default_hook)
706        }));
707
708        // Create a tokio runtime for async merger tasks.
709        let runtime_clone = runtime.clone();
710        let tokio_merger_runtime: TokioRuntime = {
711            info!("starting dbsp merger tokio runtime, workers: {num_merger_threads}",);
712
713            TokioBuilder::new_multi_thread()
714                .worker_threads(num_merger_threads)
715                .thread_name_fn(|| {
716                    use std::sync::atomic::{AtomicUsize, Ordering};
717                    static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
718                    let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
719                    format!("merger-{}", id)
720                })
721                .on_thread_start(move || {
722                    set_current_thread_type(ThreadType::Background);
723                    RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime_clone.clone()));
724                })
725                .thread_stack_size(6 * 1024 * 1024)
726                .enable_all()
727                .build()
728                .unwrap()
729        };
730
731        runtime
732            .inner()
733            .tokio_merger_runtime
734            .lock()
735            .unwrap()
736            .replace(tokio_merger_runtime);
737
738        // Monitor process RSS and update memory pressure.
739        runtime.spawn_aux_thread("rss-monitor", Parker::new(), |parker| {
740            let runtime = Runtime::runtime().unwrap();
741
742            let mut pressure_span = None;
743            while !Runtime::kill_in_progress() {
744                let process_rss = process_rss_bytes().unwrap_or_default();
745                runtime
746                    .inner()
747                    .process_rss
748                    .store(process_rss, Ordering::Relaxed);
749
750                let previous_pressure = runtime.inner().memory_pressure();
751                let current_memory_pressure = runtime.inner().raw_memory_pressure(process_rss);
752
753                // Update effective memory pressure based on the previous and current memory pressure levels.
754                //
755                // Current memory pressure is computed based on the current process RSS.
756                // Effective memory pressure determines how aggressively the circuit pushes batches to disk.
757                // We introduce a delay between the current memory pressure decreases past the threshold of the
758                // current level, and the effective memory pressure follows.
759                //
760                // - When memory pressure is low or medium, the effective memory pressure follows the current memory pressure.
761                // - When memory pressure is high or critical, we keep the effective memory pressure at the previous level until
762                //   the current memory pressure goes two levels down: high -> low or critical -> moderate.
763                let new_memory_pressure = match (previous_pressure, current_memory_pressure) {
764                    (MemoryPressure::Low | MemoryPressure::Moderate, _) => current_memory_pressure,
765                    (MemoryPressure::High, MemoryPressure::Critical) => MemoryPressure::Critical,
766                    (MemoryPressure::High, MemoryPressure::High | MemoryPressure::Moderate) => {
767                        MemoryPressure::High
768                    }
769                    (MemoryPressure::High, MemoryPressure::Low) => MemoryPressure::Low,
770                    (MemoryPressure::Critical, MemoryPressure::Critical | MemoryPressure::High) => {
771                        MemoryPressure::Critical
772                    }
773                    (MemoryPressure::Critical, MemoryPressure::Moderate | MemoryPressure::Low) => {
774                        current_memory_pressure
775                    }
776                };
777                if pressure_span.is_none() || new_memory_pressure != previous_pressure {
778                    pressure_span = Some(
779                        LongSpanBuilder::new("memory-pressure")
780                            .with_tooltip(new_memory_pressure.to_string())
781                            .build(),
782                    );
783                }
784
785                runtime
786                    .inner()
787                    .memory_pressure
788                    .store(new_memory_pressure as u8, Ordering::Relaxed);
789
790                // At high and critical levels, wakeup merger threads to push all in-memory batches to disk.
791                if previous_pressure < MemoryPressure::High
792                    && new_memory_pressure >= MemoryPressure::High
793                {
794                    runtime
795                        .inner()
796                        .memory_pressure_epoch
797                        .fetch_add(1, Ordering::Relaxed);
798                    runtime.inner().memory_pressure_notify.notify_waiters();
799                }
800                parker.park_timeout(Duration::from_secs(1));
801            }
802            drop(pressure_span);
803        });
804
805        let workers = workers
806            .map(|worker_index| {
807                let runtime = runtime.clone();
808                let build_circuit = circuit.clone();
809                let parker = Parker::new();
810                let unparker = parker.unparker().clone();
811                let local_worker_offset =
812                    worker_index - runtime.inner().layout.local_workers().start;
813                let fbuf_slab_allocator =
814                    runtime.get_fbuf_slab_allocator(local_worker_offset, ThreadType::Foreground);
815                let handle = Builder::new()
816                    .name(format!("dbsp-worker-{worker_index}"))
817                    .spawn(move || {
818                        // Set the worker's runtime handle and index
819                        WORKER_INDEX.set(worker_index);
820                        set_current_thread_type(ThreadType::Foreground);
821                        set_thread_slab_pool(Some(fbuf_slab_allocator));
822                        runtime.inner().pin_cpu();
823                        RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
824
825                        // Build the worker's circuit
826                        build_circuit(parker);
827                    })
828                    .unwrap_or_else(|error| {
829                        panic!("failed to spawn worker thread {worker_index}: {error}");
830                    });
831                (handle, unparker)
832            })
833            .collect::<Vec<_>>();
834
835        Ok(RuntimeHandle::new(runtime, workers))
836    }
837
838    pub fn downgrade(&self) -> WeakRuntime {
839        WeakRuntime(Arc::downgrade(&self.0))
840    }
841
842    /// Returns a reference to the multithreaded runtime that
843    /// manages the current worker thread, or `None` if the thread
844    /// runs without a runtime.
845    ///
846    /// Worker threads created by the [`Runtime::run`] method can access
847    /// the services provided by this API via an instance of `struct Runtime`,
848    /// which they can obtain by calling `Runtime::runtime()`.  DBSP circuits
849    /// created without a managed runtime run in the context of the client
850    /// thread.  When invoked by such a thread, this method returns `None`.
851    #[allow(clippy::self_named_constructors)]
852    pub fn runtime() -> Option<Runtime> {
853        RUNTIME.with(|rt| rt.borrow().clone())
854    }
855
856    /// Returns this runtime's storage backend, if storage is configured.
857    ///
858    /// # Panic
859    ///
860    /// Panics if this thread is not in a [Runtime].
861    pub fn storage_backend() -> Result<Arc<dyn StorageBackend>, StorageError> {
862        Runtime::runtime()
863            .unwrap()
864            .inner()
865            .storage
866            .as_ref()
867            .map_or(Err(StorageError::StorageDisabled), |storage| {
868                Ok(storage.backend.clone())
869            })
870    }
871
872    /// Returns this thread's buffer-cache handle:
873    ///
874    /// - If the thread is a foreground thread or a background task, returns
875    ///   that thread or task's buffer cache.
876    ///
877    /// - If the thread's [Runtime] does not have storage configured, the cache
878    ///   size is trivially small.
879    ///
880    /// - If the thread is not in a [Runtime], then the cache is shared among
881    ///   all such threads. (Such a thread might be in a circuit that uses
882    ///   storage, but there's no way to know because only [Runtime] makes that
883    ///   available at a thread level.)
884    ///
885    /// - If the thread is a background thread, but no buffer cache is set, this
886    ///   ordinarily indicates a bug.  However, this can happen when a
887    ///   background task is dropped, because it's normal for task-local
888    ///   variables to be unavailable in `Drop` during task shutdown.  The
889    ///   function returns `None` in this case (and only in this case).
890    pub fn buffer_cache() -> Option<Arc<BufferCache>> {
891        if let Some(buffer_cache) = BUFFER_CACHE.with(|bc| bc.borrow().clone()) {
892            // Fast path common case for foreground threads.
893            Some(buffer_cache)
894        } else if let Ok(buffer_cache) = TOKIO_BUFFER_CACHE.try_get() {
895            // Background tasks case.
896            Some(buffer_cache)
897        } else if let Some(rt) = Runtime::runtime()
898            && let Some(thread_type) = current_thread_type()
899        {
900            // Slow path for threads running in a [Runtime].
901            match thread_type {
902                ThreadType::Foreground => {
903                    let buffer_cache =
904                        rt.get_buffer_cache(Runtime::local_worker_offset(), thread_type);
905                    BUFFER_CACHE.set(Some(buffer_cache.clone()));
906                    Some(buffer_cache)
907                }
908                ThreadType::Background => {
909                    // We are in a background thread in a Tokio runtime but no
910                    // buffer cache is set in [TOKIO_BUFFER_CACHE].  This is
911                    // probably a bug, but it can happen when a background task
912                    // is canceled.  It is the reason that this function returns
913                    // an `Option`.
914                    None
915                }
916            }
917        } else {
918            // Fallback path for threads outside a [Runtime].
919            //
920            // This cache is shared by all auxiliary threads in the runtime.  In
921            // particular, output connector threads use it to maintain their
922            // output buffers.
923            //
924            // FIXME: We may need a tunable strategy for aux threads. We cannot
925            // simply give each of them the same cache as DBSP worker threads,
926            // as there can be dozens of aux threads (currently one per output
927            // connector), which do not necessarily need a large cache. OTOH,
928            // sharing the same cache across all of them may potentially cause
929            // performance issues.
930            static AUXILIARY_CACHE: LazyLock<Arc<BufferCache>> =
931                LazyLock::new(|| Arc::new(BufferCache::new(1024 * 1024 * 256)));
932
933            let buffer_cache = AUXILIARY_CACHE.clone();
934            BUFFER_CACHE.set(Some(buffer_cache.clone()));
935            Some(buffer_cache)
936        }
937    }
938
939    /// Spawn an auxiliary thread inside the runtime.
940    ///
941    /// The auxiliary thread will have access to the runtime's resources, including the
942    /// storage backend. The current use case for this is to be able to use spines outside
943    /// of the DBSP worker threads, e.g., to maintain output buffers.
944    ///
945    /// # Arguments
946    ///
947    /// * `thread_name` - The name of the thread.
948    /// * `parker` - The thread will use this parker when waiting for work. Use it to unpark
949    ///   the thread when terminating the runtime.
950    /// * `f` - The function to execute in the thread.
951    pub fn spawn_aux_thread<F>(&self, thread_name: &str, parker: Parker, f: F)
952    where
953        F: FnOnce(Parker) + Send + 'static,
954    {
955        let runtime = self.clone();
956        let unparker = parker.unparker().clone();
957        let handle = Builder::new()
958            .name(thread_name.to_string())
959            .spawn(move || {
960                RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
961                f(parker)
962            })
963            .expect("failed to spawn auxiliary thread");
964
965        self.inner()
966            .aux_threads
967            .lock()
968            .unwrap()
969            .push((handle, unparker))
970    }
971
972    /// Returns this runtime's buffer-cache handle for thread type `thread_type`
973    /// in worker with local offset `local_worker_offset`.
974    ///
975    /// Usually it's easier and faster to call [Runtime::buffer_cache] instead.
976    pub fn get_buffer_cache(
977        &self,
978        local_worker_offset: usize,
979        thread_type: ThreadType,
980    ) -> Arc<BufferCache> {
981        self.0.buffer_caches[local_worker_offset][thread_type].clone()
982    }
983
984    pub(crate) fn get_fbuf_slab_allocator(
985        &self,
986        local_worker_offset: usize,
987        thread_type: ThreadType,
988    ) -> Arc<FBufSlabs> {
989        self.0.fbuf_slab_allocators[local_worker_offset][thread_type].clone()
990    }
991
992    /// Returns `(current, max)`, reporting the amount of the buffer cache
993    /// that is currently used and its maximum size, both in bytes.
994    pub fn cache_occupancy(&self) -> (usize, usize) {
995        if self.0.storage.is_some() {
996            let mut seen = HashSet::new();
997            self.0
998                .buffer_caches
999                .iter()
1000                .flat_map(|map| map.values())
1001                .filter(|cache| seen.insert(cache.backend_id()))
1002                .map(|cache| cache.occupancy())
1003                .fold((0, 0), |(a_cur, a_max), (b_cur, b_max)| {
1004                    (a_cur + b_cur, a_max + b_max)
1005                })
1006        } else {
1007            (0, 0)
1008        }
1009    }
1010
1011    /// Returns aggregate statistics for the `FBuf` slab pools used by storage.
1012    pub fn fbuf_slabs_stats(&self) -> FBufSlabsStats {
1013        if self.0.storage.is_some() {
1014            let mut seen = HashSet::new();
1015            self.0
1016                .fbuf_slab_allocators
1017                .iter()
1018                .flat_map(|map| map.values())
1019                .filter(|allocator| seen.insert(allocator.backend_id()))
1020                .map(|allocator| allocator.stats())
1021                .fold(FBufSlabsStats::default(), |mut stats, pool_stats| {
1022                    stats += pool_stats;
1023                    stats
1024                })
1025        } else {
1026            FBufSlabsStats::default()
1027        }
1028    }
1029
1030    /// Returns 0-based index of the current worker thread within its runtime.
1031    /// For threads that run without a runtime, this method returns `0`.  In a
1032    /// multihost runtime, this is a global index across all hosts.
1033    ///
1034    /// For threads that run in the tokio merger runtime, this method returns the
1035    /// index of the foreground worker thread that this task is doing compaction for.
1036    pub fn worker_index() -> usize {
1037        match current_thread_type() {
1038            Some(ThreadType::Foreground) => WORKER_INDEX.get(),
1039            Some(ThreadType::Background) => TOKIO_WORKER_INDEX.get(),
1040            None => RUNTIME.with_borrow(|runtime| match runtime {
1041                Some(runtime) => {
1042                    // We are running in an auxiliary thread.  Treat it like the
1043                    // first thread in the local runtime.
1044                    runtime.layout().local_workers().start
1045                }
1046                None => {
1047                    // We are not running in a runtime.
1048                    0
1049                }
1050            }),
1051        }
1052    }
1053
1054    /// Returns the 0-based index of the current worker within its local host.
1055    pub fn local_worker_offset() -> usize {
1056        // Find the lowest-numbered local worker.
1057        let local_workers_start = RUNTIME
1058            .with(|rt| Some(rt.borrow().as_ref()?.layout().local_workers().start))
1059            .unwrap_or_default();
1060        Self::worker_index() - local_workers_start
1061    }
1062
1063    pub fn mode() -> Mode {
1064        RUNTIME
1065            .with(|rt| Some(rt.borrow().as_ref()?.get_mode()))
1066            .unwrap_or_default()
1067    }
1068
1069    pub fn with_dev_tweaks<F, T>(f: F) -> T
1070    where
1071        F: Fn(&DevTweaks) -> T,
1072    {
1073        static DEFAULT: Lazy<DevTweaks> = Lazy::new(DevTweaks::default);
1074        RUNTIME
1075            .with(|rt| Some(f(&rt.borrow().as_ref()?.inner().dev_tweaks)))
1076            .unwrap_or_else(|| f(&DEFAULT))
1077    }
1078
1079    pub fn get_mode(&self) -> Mode {
1080        self.inner().mode
1081    }
1082
1083    pub fn step_size(&self) -> StepSize {
1084        self.inner().step_size
1085    }
1086
1087    pub fn allow_input_during_commit(&self) -> bool {
1088        self.inner().allow_input_during_commit
1089    }
1090
1091    /// Returns the worker index as a string.
1092    ///
1093    /// This is useful for metric labels.
1094    pub fn worker_index_str() -> &'static str {
1095        static WORKER_INDEX_STRS: Lazy<[&'static str; 256]> = Lazy::new(|| {
1096            let mut data: [&'static str; 256] = [""; 256];
1097            for (i, item) in data.iter_mut().enumerate() {
1098                *item = Box::leak(i.to_string().into_boxed_str());
1099            }
1100            data
1101        });
1102
1103        WORKER_INDEX_STRS
1104            .get(Runtime::worker_index())
1105            .copied()
1106            .unwrap_or_else(|| {
1107                panic!("Limit workers to less than 256 or increase the limit in the code.")
1108            })
1109    }
1110
1111    pub fn memory_pressure() -> Option<MemoryPressure> {
1112        RUNTIME.with(|rt| Some(rt.borrow().as_ref()?.inner().memory_pressure()))
1113    }
1114
1115    pub fn current_memory_pressure(&self) -> MemoryPressure {
1116        self.inner().memory_pressure()
1117    }
1118
1119    pub fn max_rss_bytes(&self) -> Option<u64> {
1120        self.inner().max_rss
1121    }
1122
1123    pub fn memory_pressure_epoch(&self) -> u64 {
1124        self.inner().memory_pressure_epoch.load(Ordering::Relaxed)
1125    }
1126
1127    pub(crate) fn memory_pressure_notify(&self) -> Arc<Notify> {
1128        self.inner().memory_pressure_notify.clone()
1129    }
1130
1131    /// Returns the minimum number of bytes in a batch produced by a merge operator to spill it to storage.
1132    ///
1133    /// The output is determined by the current memory pressure level and the user-configured `min_storage_bytes` option.
1134    /// When memory pressure is low, the output is `min_storage_bytes`; when memory pressure is moderate, high or critical,
1135    /// the output is `0`, meaning all batches are spilled to storage.
1136    ///
1137    /// # Returns
1138    ///
1139    /// - `None` - if this thread doesn't have a Runtime or if it doesn't have storage configured.
1140    /// - `Some(0)` - spill all batches to storage.
1141    /// - `Some(N)` - spill batches with size >= N to storage.
1142    pub fn min_merge_storage_bytes() -> Option<usize> {
1143        RUNTIME.with(|rt| {
1144            let rt = rt.borrow();
1145            let inner = rt.as_ref()?.inner();
1146            let storage = inner.storage.as_ref()?;
1147
1148            if inner.memory_pressure() >= MemoryPressure::Moderate {
1149                Some(0)
1150            } else {
1151                Some(storage.options.min_storage_bytes.unwrap_or({
1152                    // This reduces the files stored on disk to a reasonable number.
1153
1154                    10 * 1024 * 1024
1155                }))
1156            }
1157        })
1158    }
1159
1160    /// Returns the minimum number of bytes in a batch inserted into a spine by a foreground worker to spill it to storage.
1161    ///
1162    /// The output is determined by the current memory pressure level and the user-configured `min_storage_bytes` option.
1163    /// When memory pressure is low the output is `usize::MAX`, when memory pressure is moderate, the output is `min_storage_bytes`,
1164    /// when memory pressure is high or critical, the output is `0`, meaning all batches are spilled to storage.
1165    ///
1166    /// # Returns
1167    ///
1168    /// - `None` - if this thread doesn't have a Runtime or if it doesn't have storage configured.
1169    /// - `Some(0)` - spill all batches to storage.
1170    /// - `Some(N)` - spill batches with size >= N to storage.
1171    pub fn min_insert_storage_bytes() -> Option<usize> {
1172        RUNTIME.with(|rt| {
1173            let rt = rt.borrow();
1174            let inner = rt.as_ref()?.inner();
1175            let storage = inner.storage.as_ref()?;
1176
1177            if inner.memory_pressure() >= MemoryPressure::High {
1178                Some(0)
1179            } else if inner.memory_pressure() >= MemoryPressure::Moderate {
1180                // Moderate pressure: spill large batches to storage in the foreground; the merger will take care of the rest.
1181                Some(
1182                    storage
1183                        .options
1184                        .min_storage_bytes
1185                        .unwrap_or(10 * 1024 * 1024),
1186                )
1187            } else {
1188                // When there is no memory pressure, we leave it to the merger to write the batches to storage
1189                // eventually.
1190                Some(usize::MAX)
1191            }
1192        })
1193    }
1194
1195    /// Returns the minimum number of bytes in a transient batch exchanged between DBSP operators during a step of the
1196    /// circuit to spill it to storage.
1197    ///
1198    /// The output is determined by the current memory pressure level and the user-configured `min_step_storage_bytes` option.
1199    /// When memory pressure is below critical, the output is `min_step_storage_bytes`, when memory pressure is critical,
1200    /// the output is `0`, meaning all batches are spilled to storage.
1201    ///
1202    /// # Returns
1203    ///
1204    /// - `None` - if this thread doesn't have a Runtime or if it doesn't have storage configured.
1205    /// - `Some(0)` - spill all batches to storage.
1206    /// - `Some(N)` - spill batches with size >= N to storage.
1207    pub fn min_step_storage_bytes() -> Option<usize> {
1208        RUNTIME.with(|rt| {
1209            let rt = rt.borrow();
1210            let inner = rt.as_ref()?.inner();
1211            let storage = inner.storage.as_ref()?;
1212
1213            if inner.memory_pressure() >= MemoryPressure::Critical {
1214                Some(0)
1215            } else {
1216                Some(storage.options.min_step_storage_bytes.unwrap_or(usize::MAX))
1217            }
1218        })
1219    }
1220
1221    pub fn file_writer_parameters() -> Parameters {
1222        let compression = Runtime::runtime()
1223            .unwrap()
1224            .inner()
1225            .storage
1226            .as_ref()
1227            .unwrap()
1228            .options
1229            .compression;
1230        let compression = match compression {
1231            StorageCompression::Default | StorageCompression::Snappy => Some(Compression::Snappy),
1232            StorageCompression::None => None,
1233        };
1234        Parameters::default().with_compression(compression)
1235    }
1236
1237    fn inner(&self) -> &RuntimeInner {
1238        &self.0
1239    }
1240
1241    /// Returns the number of workers in the runtime's [`Layout`].  In a
1242    /// multihost runtime, this is the total number of workers across all hosts.
1243    ///
1244    /// If this thread is not in a [Runtime], returns 1.
1245    pub fn num_workers() -> usize {
1246        RUNTIME.with(|rt| {
1247            rt.borrow()
1248                .as_ref()
1249                .map_or(1, |runtime| runtime.layout().n_workers())
1250        })
1251    }
1252
1253    /// Returns the [`Layout`] for this runtime.
1254    pub fn layout(&self) -> &Layout {
1255        &self.inner().layout
1256    }
1257
1258    /// Returns reference to the data store shared by all workers within the
1259    /// runtime.  In a multihost runtime, this data store is local to this
1260    /// particular host.
1261    ///
1262    /// This low-level mechanism can be used by various services that
1263    /// require common state shared across all workers on a host.
1264    ///
1265    /// The [`LocalStore`] type is an alias to [`TypedDashMap`], a
1266    /// concurrent map type that can store key/value pairs of different
1267    /// types.  See `typedmap` crate documentation for details.
1268    pub fn local_store(&self) -> &LocalStore {
1269        &self.inner().store
1270    }
1271
1272    /// Returns the path to the storage directory for this runtime.
1273    pub fn storage_path(&self) -> Option<&Path> {
1274        self.inner()
1275            .storage
1276            .as_ref()
1277            .map(|storage| storage.config.path())
1278    }
1279
1280    /// A per-worker sequential counter.
1281    ///
1282    /// This method can be used to generate unique identifiers that will be the
1283    /// same across all worker threads.  Repeated calls to this function
1284    /// from the same worker generate numbers 0, 1, 2, ...
1285    pub fn sequence_next(&self) -> usize {
1286        self.inner().worker_sequence_numbers[Self::local_worker_offset()]
1287            .fetch_add(1, Ordering::Relaxed)
1288    }
1289
1290    /// `true` if the current worker thread has received a kill signal
1291    /// and should exit asap.  Schedulers should use this method before
1292    /// scheduling the next operator and after parking.
1293    pub fn kill_in_progress() -> bool {
1294        // Only a circuit with a `Runtime` can receive a kill signal, which is
1295        // OK because a kill request can only be sent via a `RuntimeHandle`
1296        // anyway.
1297        RUNTIME.with(|runtime| {
1298            runtime
1299                .borrow()
1300                .as_ref()
1301                .map(|runtime| runtime.inner().kill_signal.load(Ordering::SeqCst))
1302                .unwrap_or(false)
1303        })
1304    }
1305
1306    pub fn cancellation_token(&self) -> CancellationToken {
1307        self.inner().cancellation_token.clone()
1308    }
1309
1310    pub fn worker_panic_info(
1311        &self,
1312        worker: usize,
1313        thread_type: ThreadType,
1314    ) -> Option<WorkerPanicInfo> {
1315        if let Ok(guard) = self.inner().panic_info[worker][thread_type].read() {
1316            guard.clone()
1317        } else {
1318            warn!("poisoned panic_lock lock for {thread_type} worker {worker}");
1319            None
1320        }
1321    }
1322
1323    // Record information about a worker thread panic in `panic_info`
1324    fn panic(&self, panic_info: &PanicHookInfo) {
1325        let local_worker_offset = Self::local_worker_offset();
1326        let Some(thread_type) = current_thread_type() else {
1327            // We only install panic hooks on foreground and background threads,
1328            // so this shouldn't happen, but we cannot panic here.
1329            error!("panic hook called outside of a runtime or on an aux thread");
1330            return;
1331        };
1332        let panic_info = WorkerPanicInfo::new(panic_info);
1333        let _ = self.inner().panic_info[local_worker_offset][thread_type]
1334            .write()
1335            .map(|mut guard| *guard = Some(panic_info));
1336        self.inner().panicked.store(true, Ordering::Release);
1337    }
1338
1339    /// Handle to the tokio merger runtime associated with this DBSP runtime.
1340    ///
1341    /// Returns `None` if the runtime has already been torn down. Callers may
1342    /// race `RuntimeHandle::join`, which takes the runtime out of its slot
1343    /// before dropping it; an in-flight merger task that reaches this accessor
1344    /// after the take observes `None` and should bail out of its work.
1345    pub(crate) fn tokio_merger_runtime(&self) -> Option<tokio::runtime::Handle> {
1346        self.inner()
1347            .tokio_merger_runtime
1348            .lock()
1349            .unwrap()
1350            .as_ref()
1351            .map(|rt| rt.handle().clone())
1352    }
1353
1354    /// Takes and returns the TCP listener socket configured via
1355    /// [CircuitConfig], if any.
1356    ///
1357    /// [CircuitConfig]: crate::circuit::dbsp_handle::CircuitConfig
1358    pub(crate) fn take_exchange_listener(&self) -> Option<TcpListener> {
1359        self.inner().exchange_listener.lock().unwrap().take()
1360    }
1361
1362    /// Returns [DevTweaks] for this `Runtime`.
1363    ///
1364    /// Use [Runtime::with_dev_tweaks] if there's not a `Runtime` handy already.
1365    pub fn dev_tweaks(&self) -> &DevTweaks {
1366        &self.inner().dev_tweaks
1367    }
1368}
1369
1370/// A synchronization primitive that allows multiple threads within a runtime to agree
1371/// when a condition is satisfied.
1372pub(crate) struct Consensus(Broadcast<bool>);
1373
1374impl Consensus {
1375    pub fn new(name: impl Display) -> Self {
1376        Self(Broadcast::new(name))
1377    }
1378
1379    /// Returns `true` if all workers vote `true`.
1380    ///
1381    /// # Arguments
1382    ///
1383    /// * `local` - Local vote by the current worker.
1384    pub async fn check(&self, local: bool) -> Result<bool, SchedulerError> {
1385        Ok(self.0.collect(local).await?.into_iter().all(identity))
1386    }
1387}
1388
1389/// A synchronization primitive that allows multiple threads within a runtime to broadcast
1390/// a value to all other workers.
1391pub(crate) enum Broadcast<T> {
1392    SingleThreaded,
1393    MultiThreaded {
1394        exchange: Arc<Exchange<T>>,
1395        identifier: Arc<String>,
1396    },
1397}
1398
1399impl<T> Broadcast<T>
1400where
1401    T: Clone + Debug + Send + serde::Serialize + for<'de> serde::Deserialize<'de> + 'static,
1402{
1403    pub fn new(name: impl Display) -> Self {
1404        match Runtime::runtime() {
1405            Some(runtime) if Runtime::num_workers() > 1 => {
1406                let exchange_id = runtime.sequence_next().try_into().unwrap();
1407                let exchange =
1408                    Exchange::with_runtime(&runtime, exchange_id, ExchangeActivity::AllSteps);
1409                let identifier = Arc::new(format!("broadcast {name} (exchange {exchange_id})"));
1410
1411                Self::MultiThreaded {
1412                    exchange,
1413                    identifier,
1414                }
1415            }
1416            _ => Self::SingleThreaded,
1417        }
1418    }
1419
1420    /// Returns values broadcast by all workers (including the current worker), indexed by worker id.
1421    ///
1422    /// # Arguments
1423    ///
1424    /// * `local` - Value broadcast by the current worker.
1425    pub async fn collect(&self, local: T) -> Result<Vec<T>, SchedulerError> {
1426        match self {
1427            Self::SingleThreaded => Ok(vec![local]),
1428            Self::MultiThreaded {
1429                exchange,
1430                identifier,
1431            } => {
1432                // This span shows elapsed time from sending our broadcast to
1433                // getting all the others' results back.
1434                let _span = Span::new("broadcast")
1435                    .with_category("Exchange")
1436                    .with_tooltip(|| format!("collect for {}", identifier));
1437
1438                Runtime::runtime()
1439                    .unwrap()
1440                    .cancellation_token()
1441                    .run_until_cancelled_owned(async {
1442                        exchange
1443                            .send_all_with_serializer(identifier, repeat(local.clone()), |local| {
1444                                let mut fbuf = FBuf::new();
1445                                serde_json::to_writer(&mut fbuf, &local).unwrap();
1446                                fbuf
1447                            })
1448                            .await;
1449
1450                        exchange
1451                            .receive_all(|data| serde_json::from_slice(&data).unwrap(), None)
1452                            .await
1453                            .0
1454                    })
1455                    .await
1456                    .ok_or(SchedulerError::Killed)
1457            }
1458        }
1459    }
1460}
1461
1462/// Handle returned by `Runtime::run`.
1463#[derive(Debug)]
1464pub struct RuntimeHandle {
1465    runtime: Runtime,
1466    workers: Vec<(JoinHandle<()>, Unparker)>,
1467}
1468
1469impl RuntimeHandle {
1470    fn new(runtime: Runtime, workers: Vec<(JoinHandle<()>, Unparker)>) -> Self {
1471        Self { runtime, workers }
1472    }
1473
1474    /// Unpark worker thread.
1475    ///
1476    /// Workers release the CPU by parking when they have no work to do.
1477    /// This method unparks a thread after sending a command to it or
1478    /// when killing a circuit.
1479    pub(super) fn unpark_worker(&self, worker: usize) {
1480        self.workers[worker].1.unpark();
1481    }
1482
1483    /// Returns reference to the runtime.
1484    pub fn runtime(&self) -> &Runtime {
1485        &self.runtime
1486    }
1487
1488    /// Terminate the runtime and all worker threads without waiting for any
1489    /// in-progress computation to complete.
1490    ///
1491    /// Signals all workers to exit.  Any operators already running are
1492    /// evaluated to completion, after which the worker thread terminates
1493    /// even if the circuit has not been fully evaluated for the current
1494    /// clock cycle.
1495    pub fn kill(self) -> ThreadResult<()> {
1496        self.kill_async();
1497        self.join()
1498    }
1499
1500    // Signals all worker threads to exit, and returns immediately without
1501    // waiting for them to exit.
1502    pub fn kill_async(&self) {
1503        self.runtime
1504            .inner()
1505            .kill_signal
1506            .store(true, Ordering::SeqCst);
1507        self.runtime.inner().cancellation_token.cancel();
1508        for (_worker, unparker) in self.workers.iter() {
1509            unparker.unpark();
1510        }
1511
1512        self.runtime
1513            .inner()
1514            .aux_threads
1515            .lock()
1516            .unwrap()
1517            .iter()
1518            .for_each(|(_h, unparker)| {
1519                unparker.unpark();
1520            });
1521    }
1522
1523    /// Wait for all workers in the runtime to terminate.
1524    ///
1525    /// The calling thread blocks until all worker threads have terminated.
1526    pub fn join(self) -> ThreadResult<()> {
1527        // Insist on joining all threads even if some of them fail.
1528        #[allow(clippy::needless_collect)]
1529        let results: Vec<ThreadResult<()>> = self
1530            .workers
1531            .into_iter()
1532            .map(|(h, _unparker)| h.join())
1533            .collect();
1534
1535        // Once all fg threads have terminated, signal the aux threads to terminate as well.
1536        // Normally this is not needed, since this function is usually called from `kill_async`,
1537        // which already signals the aux threads to terminate, but it is useful when it is called
1538        // directly, e.g., in some tests.
1539        self.runtime
1540            .inner()
1541            .kill_signal
1542            .store(true, Ordering::SeqCst);
1543
1544        self.runtime
1545            .inner()
1546            .aux_threads
1547            .lock()
1548            .unwrap()
1549            .iter()
1550            .for_each(|(_h, unparker)| {
1551                unparker.unpark();
1552            });
1553
1554        // Wait for aux threads.
1555        self.runtime
1556            .inner()
1557            .aux_threads
1558            .lock()
1559            .unwrap()
1560            .drain(..)
1561            .for_each(|(h, _unparker)| {
1562                let _ = h.join();
1563            });
1564
1565        // Terminate the tokio merger runtime.
1566        //
1567        // The `MutexGuard` returned by `lock()` must be dropped before the
1568        // tokio runtime: dropping the runtime blocks until every blocking-pool
1569        // task yields, and those tasks may call `tokio_merger_runtime()`, which
1570        // re-locks this same mutex. Holding the guard across the runtime drop
1571        // would deadlock. Binding `take()` to a `let` releases the guard at the
1572        // `;`, leaving an owned `Option<TokioRuntime>` whose drop runs without
1573        // the mutex held.
1574        let tokio_merger_runtime = self
1575            .runtime
1576            .inner()
1577            .tokio_merger_runtime
1578            .lock()
1579            .unwrap()
1580            .take();
1581        drop(tokio_merger_runtime);
1582
1583        // This must happen after we wait for the background threads, because
1584        // they might try to initiate another merge before they exit, which
1585        // would require them to have access to storage, which is kept in the
1586        // local store.
1587        self.runtime.local_store().clear();
1588
1589        results.into_iter().collect()
1590    }
1591
1592    /// Retrieve panic info for a specific worker.
1593    pub fn worker_panic_info(
1594        &self,
1595        worker: usize,
1596        thread_type: ThreadType,
1597    ) -> Option<WorkerPanicInfo> {
1598        self.runtime.worker_panic_info(worker, thread_type)
1599    }
1600
1601    /// Retrieve panic info for all workers.
1602    pub fn collect_panic_info(&self) -> Vec<(usize, ThreadType, WorkerPanicInfo)> {
1603        let mut result = Vec::new();
1604
1605        for worker in 0..self.workers.len() {
1606            for thread_type in [ThreadType::Foreground, ThreadType::Background] {
1607                if let Some(panic_info) = self.worker_panic_info(worker, thread_type) {
1608                    result.push((worker, thread_type, panic_info))
1609                }
1610            }
1611        }
1612        result
1613    }
1614
1615    /// Returns true if any worker has panicked.
1616    pub fn panicked(&self) -> bool {
1617        self.runtime.inner().panicked.load(Ordering::Acquire)
1618    }
1619}
1620
1621/// Where a worker thread is.
1622#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1623pub enum WorkerLocation {
1624    /// In this process.
1625    Local,
1626    /// Across the network.
1627    Remote,
1628}
1629
1630/// An iterator for all of the workers in a runtime.
1631///
1632/// For every worker in a [Runtime], this iterator yields its [WorkerLocation].
1633#[derive(Clone, Debug)]
1634pub struct WorkerLocations {
1635    workers: Range<usize>,
1636    local_workers: Range<usize>,
1637}
1638
1639impl Default for WorkerLocations {
1640    fn default() -> Self {
1641        Self::new()
1642    }
1643}
1644
1645impl WorkerLocations {
1646    /// Constructs a new iterator for all the worker locations in the current
1647    /// [Runtime].  Use [Iterator::enumerate] to also obtain the associated
1648    /// worker indexes.
1649    pub fn new() -> Self {
1650        if let Some(runtime) = Runtime::runtime() {
1651            Self::for_layout(runtime.layout())
1652        } else {
1653            Self {
1654                workers: 0..1,
1655                local_workers: 0..1,
1656            }
1657        }
1658    }
1659
1660    /// Constructs a new iterator from `layout`.
1661    pub fn for_layout(layout: &Layout) -> Self {
1662        Self {
1663            workers: 0..layout.n_workers(),
1664            local_workers: layout.local_workers(),
1665        }
1666    }
1667
1668    /// Returns the location of the worker with the given index.
1669    pub fn get(&self, worker: usize) -> WorkerLocation {
1670        self[worker]
1671    }
1672}
1673
1674impl Index<usize> for WorkerLocations {
1675    type Output = WorkerLocation;
1676
1677    fn index(&self, worker: usize) -> &Self::Output {
1678        debug_assert!(worker < self.workers.end);
1679        if self.local_workers.contains(&worker) {
1680            &WorkerLocation::Local
1681        } else {
1682            &WorkerLocation::Remote
1683        }
1684    }
1685}
1686
1687impl Iterator for WorkerLocations {
1688    type Item = WorkerLocation;
1689
1690    fn next(&mut self) -> Option<Self::Item> {
1691        let worker = self.workers.next()?;
1692        Some(self.get(worker))
1693    }
1694
1695    fn size_hint(&self) -> (usize, Option<usize>) {
1696        let len = self.workers.len();
1697        (len, Some(len))
1698    }
1699}
1700
1701impl ExactSizeIterator for WorkerLocations {}
1702
1703#[cfg(test)]
1704mod tests {
1705    use super::{Runtime, RuntimeInner};
1706    use crate::{
1707        Circuit, RootCircuit,
1708        circuit::{
1709            CircuitConfig,
1710            dbsp_handle::CircuitStorageConfig,
1711            metadata::{LOOSE_MEMORY_RECORDS_COUNT, MERGING_MEMORY_RECORDS_COUNT},
1712            schedule::{DynamicScheduler, Scheduler},
1713        },
1714        operator::Generator,
1715        profile::DbspProfile,
1716        storage::backend::FileId,
1717    };
1718    use enum_map::Enum;
1719    use feldera_buffer_cache::{CacheEntry, ThreadType};
1720    use feldera_storage::fbuf::{FBuf, slab::set_thread_slab_pool};
1721    use feldera_types::config::{
1722        StorageCacheConfig, StorageConfig, StorageOptions,
1723        dev_tweaks::{BufferCacheAllocationStrategy, BufferCacheStrategy},
1724    };
1725    use feldera_types::memory_pressure::MemoryPressure;
1726    use std::{
1727        cell::RefCell,
1728        rc::Rc,
1729        sync::{Arc, LazyLock, Mutex},
1730        thread::sleep,
1731        time::{Duration, Instant},
1732    };
1733
1734    struct TestCacheEntry(usize);
1735
1736    impl CacheEntry for TestCacheEntry {
1737        fn cost(&self) -> usize {
1738            self.0
1739        }
1740    }
1741
1742    // Serialize all tests that use MOCK_PROCESS_RSS_BYTES so they don't race with each other.
1743    static MOCK_RSS_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1744
1745    fn set_mock_process_rss_bytes(bytes: u64) {
1746        // Safety: We assume that only DBSP code accesses the environment variables.
1747        unsafe {
1748            std::env::set_var("MOCK_PROCESS_RSS_BYTES", bytes.to_string());
1749        }
1750    }
1751
1752    struct MockRssVarGuard;
1753
1754    impl Drop for MockRssVarGuard {
1755        fn drop(&mut self) {
1756            // Safety: We assume that only DBSP code accesses the environment variables.
1757            unsafe {
1758                std::env::remove_var("MOCK_PROCESS_RSS_BYTES");
1759            }
1760        }
1761    }
1762
1763    fn query_runtime_memory_state(runtime: &Runtime) -> (MemoryPressure, usize, usize, usize) {
1764        let (sender, receiver) = std::sync::mpsc::channel();
1765        runtime.spawn_aux_thread(
1766            "memory-pressure-test-query",
1767            super::Parker::new(),
1768            move |_parker| {
1769                let _ = sender.send((
1770                    Runtime::memory_pressure().expect("query thread should run inside runtime"),
1771                    Runtime::min_merge_storage_bytes().expect("runtime has storage configured"),
1772                    Runtime::min_insert_storage_bytes().expect("runtime has storage configured"),
1773                    Runtime::min_step_storage_bytes().expect("runtime has storage configured"),
1774                ));
1775            },
1776        );
1777        receiver
1778            .recv_timeout(Duration::from_secs(5))
1779            .expect("failed to query memory state from runtime thread")
1780    }
1781
1782    fn count_metric(
1783        profile: &DbspProfile,
1784        metric: &'static crate::circuit::metadata::MetricId,
1785    ) -> usize {
1786        let root_node = crate::circuit::GlobalNodeId::root();
1787        profile
1788            .worker_profiles
1789            .iter()
1790            .map(|worker| {
1791                worker
1792                    .get_node_profile(&root_node)
1793                    .map(|meta| {
1794                        meta.iter()
1795                            .filter_map(|((metric_id, _labels), value)| {
1796                                if metric_id == metric {
1797                                    match value {
1798                                        crate::circuit::metadata::MetaItem::Count(count) => {
1799                                            Some(*count)
1800                                        }
1801                                        _ => None,
1802                                    }
1803                                } else {
1804                                    None
1805                                }
1806                            })
1807                            .sum::<usize>()
1808                    })
1809                    .unwrap_or(0)
1810            })
1811            .sum()
1812    }
1813
1814    fn in_memory_spine_records(profile: &DbspProfile) -> usize {
1815        count_metric(profile, &LOOSE_MEMORY_RECORDS_COUNT)
1816            + count_metric(profile, &MERGING_MEMORY_RECORDS_COUNT)
1817    }
1818
1819    #[test]
1820    #[cfg_attr(miri, ignore)]
1821    fn test_runtime_dynamic() {
1822        test_runtime::<DynamicScheduler>();
1823    }
1824
1825    #[test]
1826    #[cfg_attr(miri, ignore)]
1827    fn storage_no_cleanup() {
1828        // Case 1: storage specified, runtime should not clean up storage when exiting
1829        let path = tempfile::tempdir().unwrap().keep();
1830        let path_clone = path.clone();
1831        let cconf = CircuitConfig::with_workers(4).with_storage(Some(
1832            CircuitStorageConfig::for_config(
1833                StorageConfig {
1834                    path: path.to_string_lossy().into_owned(),
1835                    cache: StorageCacheConfig::default(),
1836                },
1837                StorageOptions::default(),
1838            )
1839            .unwrap(),
1840        ));
1841
1842        let hruntime = Runtime::run(cconf, move |_parker| {
1843            let runtime = Runtime::runtime().unwrap();
1844            assert_eq!(runtime.storage_path(), Some(path_clone.as_ref()));
1845        })
1846        .expect("failed to start runtime");
1847        hruntime.join().unwrap();
1848        assert!(path.exists(), "persistent storage is not cleaned up");
1849    }
1850
1851    #[test]
1852    fn s3_fifo_is_the_default_buffer_cache_strategy() {
1853        let inner = RuntimeInner::new(CircuitConfig::with_workers(1)).unwrap();
1854        assert_eq!(
1855            inner.buffer_caches[0][ThreadType::Foreground].strategy(),
1856            BufferCacheStrategy::S3Fifo
1857        );
1858    }
1859
1860    #[test]
1861    fn default_s3_fifo_cache_shares_backend_per_worker_pair() {
1862        let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
1863        assert!(
1864            inner.buffer_caches[0][ThreadType::Foreground]
1865                .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1866        );
1867        assert!(
1868            inner.buffer_caches[1][ThreadType::Foreground]
1869                .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1870        );
1871        assert!(
1872            !inner.buffer_caches[0][ThreadType::Foreground]
1873                .shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground])
1874        );
1875    }
1876
1877    #[test]
1878    fn lru_can_still_be_selected_explicitly() {
1879        let inner = RuntimeInner::new(
1880            CircuitConfig::with_workers(1).with_buffer_cache_strategy(BufferCacheStrategy::Lru),
1881        )
1882        .unwrap();
1883        assert_eq!(
1884            inner.buffer_caches[0][ThreadType::Foreground].strategy(),
1885            BufferCacheStrategy::Lru
1886        );
1887        assert!(
1888            !inner.buffer_caches[0][ThreadType::Foreground]
1889                .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1890        );
1891    }
1892
1893    #[test]
1894    fn lru_uses_default_total_cache_capacity_when_cache_mib_is_unset() {
1895        let workers = 3usize;
1896        let path = tempfile::tempdir().unwrap();
1897        let storage = CircuitStorageConfig::for_config(
1898            StorageConfig {
1899                path: path.path().to_string_lossy().into_owned(),
1900                cache: StorageCacheConfig::default(),
1901            },
1902            StorageOptions::default(),
1903        )
1904        .unwrap();
1905        let runtime = Runtime(Arc::new(
1906            RuntimeInner::new(
1907                CircuitConfig::with_workers(workers)
1908                    .with_storage(Some(storage))
1909                    .with_buffer_cache_strategy(BufferCacheStrategy::Lru),
1910            )
1911            .unwrap(),
1912        ));
1913
1914        assert_eq!(
1915            runtime.cache_occupancy(),
1916            (0, workers * ThreadType::LENGTH * 256usize * 1024 * 1024)
1917        );
1918    }
1919
1920    #[test]
1921    fn s3_fifo_cache_can_share_cache_per_worker_pair_when_requested() {
1922        let config = CircuitConfig::with_workers(2).with_buffer_cache_allocation_strategy(
1923            BufferCacheAllocationStrategy::SharedPerWorkerPair,
1924        );
1925        let inner = RuntimeInner::new(config).unwrap();
1926        assert!(
1927            inner.buffer_caches[0][ThreadType::Foreground]
1928                .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1929        );
1930        assert!(
1931            inner.buffer_caches[1][ThreadType::Foreground]
1932                .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1933        );
1934    }
1935
1936    #[test]
1937    fn fbuf_slabs_are_shared_per_worker_pair() {
1938        let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
1939        assert!(Arc::ptr_eq(
1940            &inner.fbuf_slab_allocators[0][ThreadType::Foreground],
1941            &inner.fbuf_slab_allocators[0][ThreadType::Background],
1942        ));
1943        assert!(Arc::ptr_eq(
1944            &inner.fbuf_slab_allocators[1][ThreadType::Foreground],
1945            &inner.fbuf_slab_allocators[1][ThreadType::Background],
1946        ));
1947        assert!(!Arc::ptr_eq(
1948            &inner.fbuf_slab_allocators[0][ThreadType::Foreground],
1949            &inner.fbuf_slab_allocators[1][ThreadType::Foreground],
1950        ));
1951    }
1952
1953    #[test]
1954    fn fbuf_slabs_honor_bytes_per_class_dev_tweak() {
1955        let inner =
1956            RuntimeInner::new(CircuitConfig::with_workers(1).with_fbuf_slab_bytes_per_class(4096))
1957                .unwrap();
1958        let allocator = inner.fbuf_slab_allocators[0][ThreadType::Foreground].clone();
1959        let previous = set_thread_slab_pool(Some(allocator.clone()));
1960
1961        let first = FBuf::with_capacity(4096);
1962        let second = FBuf::with_capacity(4096);
1963        drop(first);
1964        drop(second);
1965
1966        set_thread_slab_pool(previous);
1967
1968        assert_eq!(allocator.stats().cached_buffers, 1);
1969    }
1970
1971    #[test]
1972    fn sieve_can_share_one_global_cache_when_requested() {
1973        let config = CircuitConfig::with_workers(2)
1974            .with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global);
1975        let inner = RuntimeInner::new(config).unwrap();
1976        let global = inner.buffer_caches[0][ThreadType::Foreground].clone();
1977        assert!(global.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background]));
1978        assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground]));
1979        assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background]));
1980    }
1981
1982    #[test]
1983    fn lru_keeps_separate_foreground_and_background_caches() {
1984        let config = CircuitConfig::with_workers(2)
1985            .with_buffer_cache_strategy(BufferCacheStrategy::Lru)
1986            .with_buffer_cache_allocation_strategy(
1987                BufferCacheAllocationStrategy::SharedPerWorkerPair,
1988            );
1989        let inner = RuntimeInner::new(config).unwrap();
1990        assert!(
1991            !inner.buffer_caches[0][ThreadType::Foreground]
1992                .shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
1993        );
1994        assert!(
1995            !inner.buffer_caches[1][ThreadType::Foreground]
1996                .shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
1997        );
1998    }
1999
2000    #[test]
2001    fn shared_sharded_cache_occupancy_is_not_double_counted() {
2002        let path = tempfile::tempdir().unwrap();
2003        let storage = CircuitStorageConfig::for_config(
2004            StorageConfig {
2005                path: path.path().to_string_lossy().into_owned(),
2006                cache: StorageCacheConfig::default(),
2007            },
2008            StorageOptions {
2009                cache_mib: Some(8),
2010                ..StorageOptions::default()
2011            },
2012        )
2013        .unwrap();
2014        let runtime = Runtime(Arc::new(
2015            RuntimeInner::new(
2016                CircuitConfig::with_workers(1)
2017                    .with_storage(Some(storage))
2018                    .with_buffer_cache_allocation_strategy(
2019                        BufferCacheAllocationStrategy::SharedPerWorkerPair,
2020                    ),
2021            )
2022            .unwrap(),
2023        ));
2024
2025        runtime.get_buffer_cache(0, ThreadType::Foreground).insert(
2026            FileId::new(),
2027            0,
2028            Arc::new(TestCacheEntry(1024)),
2029        );
2030
2031        assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
2032    }
2033
2034    #[test]
2035    fn global_sharded_cache_occupancy_is_not_double_counted() {
2036        let path = tempfile::tempdir().unwrap();
2037        let storage = CircuitStorageConfig::for_config(
2038            StorageConfig {
2039                path: path.path().to_string_lossy().into_owned(),
2040                cache: StorageCacheConfig::default(),
2041            },
2042            StorageOptions {
2043                cache_mib: Some(8),
2044                ..StorageOptions::default()
2045            },
2046        )
2047        .unwrap();
2048        let runtime = Runtime(Arc::new(
2049            RuntimeInner::new(
2050                CircuitConfig::with_workers(2)
2051                    .with_storage(Some(storage))
2052                    .with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global),
2053            )
2054            .unwrap(),
2055        ));
2056
2057        runtime.get_buffer_cache(1, ThreadType::Background).insert(
2058            FileId::new(),
2059            0,
2060            Arc::new(TestCacheEntry(1024)),
2061        );
2062
2063        assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
2064    }
2065
2066    #[test]
2067    fn shared_fbuf_slab_stats_are_not_double_counted() {
2068        let path = tempfile::tempdir().unwrap();
2069        let storage = CircuitStorageConfig::for_config(
2070            StorageConfig {
2071                path: path.path().to_string_lossy().into_owned(),
2072                cache: StorageCacheConfig::default(),
2073            },
2074            StorageOptions::default(),
2075        )
2076        .unwrap();
2077        let runtime = Runtime(Arc::new(
2078            RuntimeInner::new(CircuitConfig::with_workers(1).with_storage(Some(storage))).unwrap(),
2079        ));
2080
2081        let previous = set_thread_slab_pool(Some(
2082            runtime.get_fbuf_slab_allocator(0, ThreadType::Foreground),
2083        ));
2084
2085        let first = FBuf::with_capacity(4096);
2086        drop(first);
2087        let second = FBuf::with_capacity(3000);
2088        drop(second);
2089
2090        set_thread_slab_pool(previous);
2091
2092        let stats = runtime.fbuf_slabs_stats();
2093        let class = stats
2094            .classes
2095            .iter()
2096            .find(|class| class.size == 4096)
2097            .unwrap();
2098
2099        assert_eq!(stats.alloc_requests(), 2);
2100        assert_eq!(stats.mallocs_saved(), 1);
2101        assert_eq!(stats.frees_saved(), 2);
2102        assert_eq!(stats.cached_buffers, 1);
2103        assert_eq!(class.alloc_requests, 2);
2104        assert_eq!(class.alloc_hits, 1);
2105        assert_eq!(class.recycle_requests, 2);
2106        assert_eq!(class.recycle_hits, 2);
2107    }
2108
2109    fn test_runtime<S>()
2110    where
2111        S: Scheduler + 'static,
2112    {
2113        let hruntime = Runtime::run(4, |_parker| {
2114            let data = Rc::new(RefCell::new(vec![]));
2115            let data_clone = data.clone();
2116            let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
2117                let runtime = Runtime::runtime().unwrap();
2118                // Generator that produces values using `sequence_next`.
2119                circuit
2120                    .add_source(Generator::new(move || runtime.sequence_next()))
2121                    .inspect(move |n: &usize| data_clone.borrow_mut().push(*n));
2122                Ok(())
2123            })
2124            .unwrap()
2125            .0;
2126
2127            for _ in 0..100 {
2128                root.transaction().unwrap();
2129            }
2130
2131            // The scheduler allocates the first value for metadata exchange; therefore the output starts from 2.
2132            assert_eq!(&*data.borrow(), &(2..102).collect::<Vec<usize>>());
2133        })
2134        .expect("failed to start runtime");
2135
2136        hruntime.join().unwrap();
2137    }
2138
2139    #[test]
2140    fn test_kill_dynamic() {
2141        test_kill::<DynamicScheduler>();
2142    }
2143
2144    // Test `RuntimeHandle::kill`.
2145    fn test_kill<S>()
2146    where
2147        S: Scheduler + 'static,
2148    {
2149        let hruntime = Runtime::run(16, |_parker| {
2150            // Create a nested circuit that iterates forever.
2151            let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
2152                circuit
2153                    .iterate_with_scheduler::<_, _, _, S>(|child| {
2154                        let mut n: usize = 0;
2155                        child
2156                            .add_source(Generator::new(move || {
2157                                n += 1;
2158                                n
2159                            }))
2160                            .inspect(|_: &usize| {});
2161                        Ok((async || Ok(false), ()))
2162                    })
2163                    .unwrap();
2164                Ok(())
2165            })
2166            .unwrap()
2167            .0;
2168
2169            loop {
2170                if root.transaction().is_err() {
2171                    return;
2172                }
2173            }
2174        })
2175        .expect("failed to start runtime");
2176
2177        sleep(Duration::from_millis(100));
2178        hruntime.kill().unwrap();
2179    }
2180
2181    // Test the memory pressure thresholds and how merger threads behave under different memory pressure levels.
2182    #[test]
2183    fn memory_pressure_thresholds_and_spill_behavior() {
2184        const GIB: u64 = 1024 * 1024 * 1024;
2185        const MIB: u64 = 1024 * 1024;
2186        const TEN_MIB: usize = 10 * 1024 * 1024;
2187
2188        let _mock_guard = MOCK_RSS_LOCK.lock().unwrap();
2189        let _clear_mock_rss = MockRssVarGuard;
2190        set_mock_process_rss_bytes(0);
2191
2192        let storage_dir = tempfile::tempdir().unwrap();
2193        let config = CircuitConfig::with_workers(1)
2194            .with_temporary_storage(storage_dir.path())
2195            .with_max_rss_bytes(Some(10 * GIB));
2196
2197        // Create a circuit with a single spine.
2198        let (mut circuit, input_handle) = Runtime::init_circuit(config, move |circuit| {
2199            let (stream, input_handle) = circuit.add_input_zset::<u64>();
2200            stream.accumulate_integrate_trace();
2201            Ok(input_handle)
2202        })
2203        .unwrap();
2204
2205        // Push 7 batches. This is below the merge threshold, so no batches should get merged,
2206        // and al of them should live in memory.
2207        for key in 0..7 {
2208            input_handle.push(key, 1);
2209            circuit.transaction().unwrap();
2210        }
2211
2212        // Baseline (mock RSS = 0): no pressure and default spill thresholds.
2213        let (pressure, min_merge, min_insert, min_step) =
2214            query_runtime_memory_state(circuit.runtime());
2215        assert_eq!(pressure, MemoryPressure::Low);
2216
2217        // Verify the spill thresholds: at low pressure, merge threshold is set to the user-configured `min_storage_bytes`,
2218        // insert threshold is set to `usize::MAX`, and step threshold is set to `usize::MAX`.
2219        assert_eq!(min_merge, TEN_MIB);
2220        assert_eq!(min_insert, usize::MAX);
2221        assert_eq!(min_step, usize::MAX);
2222
2223        // Verify the spine currently holds in-memory tuples before pressure rises.
2224        let profile = circuit.retrieve_profile().unwrap();
2225        assert!(in_memory_spine_records(&profile) == 7);
2226
2227        // Moderate pressure (8.8/10 GiB): merge threshold drops to 0, insert/step stay unchanged.
2228        set_mock_process_rss_bytes(8800 * MIB);
2229        sleep(Duration::from_secs(5));
2230
2231        let (pressure, min_merge, min_insert, min_step) =
2232            query_runtime_memory_state(circuit.runtime());
2233        assert_eq!(pressure, MemoryPressure::Moderate);
2234        assert_eq!(min_merge, 0);
2235        assert_eq!(min_insert, TEN_MIB);
2236        assert_eq!(min_step, usize::MAX);
2237
2238        // High pressure (9.4/10 GiB): insert threshold drops to 0 as well.
2239        set_mock_process_rss_bytes(9400 * MIB);
2240        sleep(Duration::from_secs(5));
2241
2242        let (pressure, min_merge, min_insert, min_step) =
2243            query_runtime_memory_state(circuit.runtime());
2244        assert_eq!(pressure, MemoryPressure::High);
2245        assert_eq!(min_merge, 0);
2246        assert_eq!(min_insert, 0);
2247        assert_eq!(min_step, usize::MAX);
2248
2249        // Under high pressure, merges should eventually spill all in-memory tuples to storage.
2250        // Poll profile until this converges to 0.
2251        let deadline = Instant::now() + Duration::from_secs(20);
2252        loop {
2253            let profile = circuit.retrieve_profile().unwrap();
2254            if in_memory_spine_records(&profile) == 0 {
2255                break;
2256            }
2257            assert!(
2258                Instant::now() < deadline,
2259                "in-memory spine records did not drain to zero under high memory pressure"
2260            );
2261            sleep(Duration::from_millis(250));
2262        }
2263
2264        // Critical pressure (9.8/10 GiB): all spill thresholds must be 0.
2265        set_mock_process_rss_bytes(9800 * MIB);
2266        sleep(Duration::from_secs(3));
2267
2268        let (pressure, min_merge, min_insert, min_step) =
2269            query_runtime_memory_state(circuit.runtime());
2270        assert_eq!(pressure, MemoryPressure::Critical);
2271        assert_eq!(min_merge, 0);
2272        assert_eq!(min_insert, 0);
2273        assert_eq!(min_step, 0);
2274
2275        circuit.kill().unwrap();
2276    }
2277}