use super::CircuitConfig;
use super::dbsp_handle::{Layout, Mode};
use crate::SchedulerError;
use crate::circuit::checkpointer::Checkpointer;
use crate::circuit::dbsp_handle::StepSize;
use crate::error::Error as DbspError;
use crate::operator::communication::{Exchange, ExchangeActivity};
use crate::storage::backend::StorageBackend;
use crate::storage::file::format::Compression;
use crate::storage::file::writer::Parameters;
use crate::utils::process_rss_bytes;
use crate::{
DetailedError,
storage::{
backend::StorageError,
buffer_cache::{BufferCache, build_buffer_caches},
dirlock::LockedDirectory,
},
};
use core_affinity::{CoreId, get_core_ids};
use crossbeam::sync::{Parker, Unparker};
use enum_map::{Enum, EnumMap, enum_map};
use feldera_buffer_cache::ThreadType;
use feldera_samply::{LongSpanBuilder, Span};
use feldera_storage::fbuf::FBuf;
use feldera_storage::fbuf::slab::{FBufSlabs, FBufSlabsStats, set_thread_slab_pool};
use feldera_types::config::{DevTweaks, StorageCompression, StorageConfig, StorageOptions};
use feldera_types::memory_pressure::{
CRITICAL_MEMORY_PRESSURE_THRESHOLD, HIGH_MEMORY_PRESSURE_THRESHOLD,
MODERATE_MEMORY_PRESSURE_THRESHOLD, MemoryPressure,
};
use indexmap::IndexSet;
use once_cell::sync::Lazy;
use serde::Serialize;
use std::convert::identity;
use std::iter::repeat;
use std::net::TcpListener;
use std::ops::{Index, Range};
use std::path::Path;
use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize};
use std::sync::{LazyLock, Mutex};
use std::time::Duration;
use std::{
backtrace::Backtrace,
borrow::Cow,
cell::{Cell, RefCell},
collections::HashSet,
error::Error as StdError,
fmt,
fmt::{Debug, Display, Error as FmtError, Formatter},
panic::{self, Location, PanicHookInfo},
sync::{
Arc, RwLock, Weak,
atomic::{AtomicBool, Ordering},
},
thread::{Builder, JoinHandle, Result as ThreadResult},
};
use tokio::runtime::Builder as TokioBuilder;
use tokio::runtime::Runtime as TokioRuntime;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, warn};
use typedmap::TypedDashMap;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum Error {
UnknownPersistentId(String),
WorkerPanic {
panic_info: Vec<(usize, ThreadType, WorkerPanicInfo)>,
},
IncompatibleStorage,
CheckpointParseError(String),
BootstrapCircuit(String),
Terminated,
}
impl DetailedError for Error {
fn error_code(&self) -> Cow<'static, str> {
match self {
Self::UnknownPersistentId(_) => Cow::from("UnknownPersistentId"),
Self::WorkerPanic { .. } => Cow::from("WorkerPanic"),
Self::Terminated => Cow::from("Terminated"),
Self::IncompatibleStorage => Cow::from("IncompatibleStorage"),
Self::CheckpointParseError(_) => Cow::from("CheckpointParseError"),
Self::BootstrapCircuit(_) => Cow::from("BootstrapCircuit"),
}
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
match self {
Self::UnknownPersistentId(persistent_id) => {
write!(f, "Unknown persistent node id: {persistent_id}")
}
Self::WorkerPanic { panic_info } => {
writeln!(f, "One or more worker threads terminated unexpectedly")?;
for (worker, thread_type, worker_panic_info) in panic_info.iter() {
writeln!(f, "{thread_type} worker thread {worker} panicked")?;
writeln!(f, "{worker_panic_info}")?;
}
Ok(())
}
Self::Terminated => f.write_str("circuit has been terminated"),
Self::IncompatibleStorage => {
f.write_str("Supplied storage directory does not fit the runtime circuit")
}
Self::CheckpointParseError(error) => {
write!(f, "Error deserializing checkpointed state: {error}")
}
Self::BootstrapCircuit(error) => {
write!(f, "Bootstrap circuit error: {error}")
}
}
}
}
impl StdError for Error {}
thread_local! {
static RUNTIME: RefCell<Option<Runtime>> = const { RefCell::new(None) };
static CURRENT_THREAD_TYPE: Cell<Option<ThreadType>> = const { Cell::new(None) };
}
thread_local! {
static WORKER_INDEX: Cell<usize> = const { Cell::new(0) };
static BUFFER_CACHE: RefCell<Option<Arc<BufferCache>>> = const { RefCell::new(None) };
}
tokio::task_local! {
pub(crate) static TOKIO_WORKER_INDEX: usize;
pub(crate) static TOKIO_BUFFER_CACHE: Arc<BufferCache>;
}
pub(crate) fn current_thread_type() -> Option<ThreadType> {
CURRENT_THREAD_TYPE.get()
}
fn set_current_thread_type(thread_type: ThreadType) {
CURRENT_THREAD_TYPE.set(Some(thread_type));
}
pub struct LocalStoreMarker;
pub type LocalStore = TypedDashMap<LocalStoreMarker>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PanicLocation {
file: String,
line: u32,
col: u32,
}
impl Display for PanicLocation {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.col)
}
}
impl PanicLocation {
fn new(loc: &Location) -> Self {
Self {
file: loc.file().to_string(),
line: loc.line(),
col: loc.column(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct WorkerPanicInfo {
message: Option<String>,
location: Option<PanicLocation>,
backtrace: String,
}
impl Display for WorkerPanicInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(message) = &self.message {
writeln!(f, "panic message: {message}")?;
} else {
writeln!(f, "panic message (none)")?;
}
if let Some(location) = &self.location {
writeln!(f, "panic location: {location}")?;
} else {
writeln!(f, "panic location: unknown")?;
}
writeln!(f, "stack trace:\n{}", self.backtrace)
}
}
impl WorkerPanicInfo {
fn new(panic_info: &PanicHookInfo) -> Self {
#[allow(clippy::manual_map)]
let message = if let Some(v) = panic_info.payload().downcast_ref::<String>() {
Some(v.clone())
} else if let Some(v) = panic_info.payload().downcast_ref::<&str>() {
Some(v.to_string())
} else {
None
};
let backtrace = Backtrace::force_capture().to_string();
let location = panic_info
.location()
.map(|location| PanicLocation::new(location));
Self {
message,
location,
backtrace,
}
}
}
#[derive(derive_more::Debug)]
struct RuntimeStorage {
pub config: StorageConfig,
pub options: StorageOptions,
#[debug(skip)]
pub backend: Arc<dyn StorageBackend>,
#[allow(dead_code)]
locked_directory: LockedDirectory,
}
struct RuntimeInner {
layout: Layout,
mode: Mode,
step_size: StepSize,
allow_input_during_commit: bool,
dev_tweaks: DevTweaks,
max_rss: Option<u64>,
process_rss: AtomicU64,
memory_pressure: AtomicU8,
memory_pressure_epoch: AtomicU64,
memory_pressure_notify: Arc<Notify>,
storage: Option<RuntimeStorage>,
store: LocalStore,
kill_signal: AtomicBool,
cancellation_token: CancellationToken,
aux_threads: Mutex<Vec<(JoinHandle<()>, Unparker)>>,
buffer_caches: Vec<EnumMap<ThreadType, Arc<BufferCache>>>,
pin_cpus_fg: Vec<CoreId>,
pin_cpus_bg: Vec<CoreId>,
fbuf_slab_allocators: Vec<EnumMap<ThreadType, Arc<FBufSlabs>>>,
worker_sequence_numbers: Vec<AtomicUsize>,
panic_info: Vec<EnumMap<ThreadType, RwLock<Option<WorkerPanicInfo>>>>,
panicked: AtomicBool,
tokio_merger_runtime: Mutex<Option<TokioRuntime>>,
exchange_listener: Mutex<Option<TcpListener>>,
}
impl Drop for RuntimeInner {
fn drop(&mut self) {
debug!("dropping RuntimeInner");
}
}
impl Debug for RuntimeInner {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("RuntimeInner")
.field("layout", &self.layout)
.field("storage", &self.storage)
.finish()
}
}
fn display_core_ids<'a>(iter: impl Iterator<Item = &'a CoreId>) -> String {
format!(
"{:?}",
iter.map(|core| core.id).collect::<Vec<_>>().as_slice()
)
}
fn map_pin_cpus(config: &CircuitConfig) -> (Vec<CoreId>, Vec<CoreId>) {
if config.layout.is_multihost() {
if !config.pin_cpus.is_empty() {
warn!("CPU pinning not yet supported with multihost DBSP");
}
return (Vec::new(), Vec::new());
}
let merger_threads = config.num_merger_threads();
let nworkers = config.layout.n_workers();
let pin_cpus = config
.pin_cpus
.iter()
.copied()
.map(|id| CoreId { id })
.collect::<IndexSet<_>>();
let expected_cpus = nworkers + merger_threads;
if pin_cpus.len() < expected_cpus {
if !pin_cpus.is_empty() {
warn!(
"ignoring CPU pinning request because {nworkers} foreground workers and {} merger workers require {} pinned CPUs but only {} were specified",
merger_threads,
expected_cpus,
pin_cpus.len()
)
}
return (Vec::new(), Vec::new());
}
let Some(core_ids) = get_core_ids() else {
warn!(
"ignoring CPU pinning request because this system's core ids list could not be obtained"
);
return (Vec::new(), Vec::new());
};
let core_ids = core_ids.iter().copied().collect::<IndexSet<_>>();
let missing_cpus = pin_cpus.difference(&core_ids).copied().collect::<Vec<_>>();
if !missing_cpus.is_empty() {
warn!(
"ignoring CPU pinning request because requested CPUs {missing_cpus:?} are not available (available CPUs are: {})",
display_core_ids(core_ids.iter())
);
return (Vec::new(), Vec::new());
}
let fg_cpus = &pin_cpus[0..nworkers];
let bg_cpus = &pin_cpus[nworkers..expected_cpus];
info!(
"pinning foreground workers to CPUs {} and background workers to CPUs {}",
display_core_ids(fg_cpus.iter()),
display_core_ids(bg_cpus.iter())
);
let fg_pinning = (0..nworkers).map(|i| fg_cpus[i]).collect();
let bg_pinning = (0..merger_threads).map(|i| bg_cpus[i]).collect();
(fg_pinning, bg_pinning)
}
impl RuntimeInner {
fn new(config: CircuitConfig) -> Result<Self, DbspError> {
let nworkers = config.layout.local_workers().len();
let buffer_cache_strategy = config.dev_tweaks.buffer_cache_strategy();
let buffer_max_buckets = config.dev_tweaks.buffer_max_buckets;
let buffer_cache_allocation_strategy = config
.dev_tweaks
.effective_buffer_cache_allocation_strategy();
let fbuf_slab_bytes_per_class = config
.dev_tweaks
.fbuf_slab_bytes_per_class
.unwrap_or(FBufSlabs::DEFAULT_BYTES_PER_CLASS);
let storage = if let Some(storage) = config.storage.clone() {
let locked_directory =
LockedDirectory::new_blocking(storage.config.path(), Duration::from_secs(60))?;
let backend = storage.backend;
if let Some(init_checkpoint) = storage.init_checkpoint
&& !backend
.exists(&Checkpointer::checkpoint_dir(init_checkpoint).child("CHECKPOINT"))?
{
return Err(DbspError::Storage(StorageError::CheckpointNotFound(
init_checkpoint,
)));
}
Some(RuntimeStorage {
config: storage.config,
options: storage.options,
backend,
locked_directory,
})
} else {
None
};
let total_cache_bytes = if let Some(storage) = &storage {
match storage.options.cache_mib {
Some(cache_mib) => cache_mib.saturating_mul(1024 * 1024),
None => 256usize
.saturating_mul(1024 * 1024)
.saturating_mul(nworkers)
.saturating_mul(ThreadType::LENGTH),
}
} else {
1
};
info!(
"Setting up buffer caches: {buffer_cache_strategy:?} {buffer_cache_allocation_strategy:?} buckets={buffer_max_buckets:?} total_size={:?} MiB",
total_cache_bytes / (1024 * 1024)
);
let buffer_caches = build_buffer_caches(
nworkers,
total_cache_bytes,
buffer_cache_strategy,
buffer_max_buckets,
buffer_cache_allocation_strategy,
);
info!("Setting up FBuf slab allocators: bytes_per_class={fbuf_slab_bytes_per_class}");
let fbuf_slab_allocators = (0..nworkers)
.map(|_| {
let allocator = Arc::new(FBufSlabs::new(fbuf_slab_bytes_per_class));
enum_map! {
ThreadType::Foreground => allocator.clone(),
ThreadType::Background => allocator.clone(),
}
})
.collect();
let (pin_cpus_fg, pin_cpus_bg) = map_pin_cpus(&config);
if !config.dev_tweaks.other_options.is_empty() {
warn!(
"Circuit dev_tweaks includes unknown options: {:?}",
&config.dev_tweaks.other_options
);
}
if config.dev_tweaks.streaming_exchange() {
info!("dev_tweaks.streaming_exchange enabled");
} else {
info!("dev_tweaks.streaming_exchange disabled");
}
Ok(Self {
pin_cpus_fg,
pin_cpus_bg,
layout: config.layout,
mode: config.mode,
step_size: config.step_size,
allow_input_during_commit: config.allow_input_during_commit,
dev_tweaks: config.dev_tweaks,
max_rss: config.max_rss_bytes,
process_rss: AtomicU64::new(process_rss_bytes().unwrap_or_default()),
memory_pressure: AtomicU8::new(MemoryPressure::Low as u8),
memory_pressure_epoch: AtomicU64::new(0),
memory_pressure_notify: Arc::new(Notify::new()),
storage,
store: TypedDashMap::new(),
kill_signal: AtomicBool::new(false),
cancellation_token: CancellationToken::new(),
aux_threads: Mutex::new(Vec::new()),
buffer_caches,
fbuf_slab_allocators,
worker_sequence_numbers: (0..nworkers).map(|_| AtomicUsize::new(0)).collect(),
panic_info: (0..nworkers)
.map(|_| EnumMap::from_fn(|_| RwLock::new(None)))
.collect(),
panicked: AtomicBool::new(false),
tokio_merger_runtime: Mutex::new(None),
exchange_listener: Mutex::new(config.exchange_listener),
})
}
fn pin_cpu(&self) {
match current_thread_type() {
Some(ThreadType::Foreground) => {
if !self.pin_cpus_fg.is_empty() {
let local_worker_offset = Runtime::local_worker_offset();
let core = self.pin_cpus_fg[local_worker_offset];
if !core_affinity::set_for_current(core) {
warn!(
"failed to pin foreground worker {local_worker_offset} to core {}",
core.id
);
}
}
}
Some(ThreadType::Background) => {
if !self.pin_cpus_bg.is_empty() {
let local_worker_offset = Runtime::local_worker_offset();
let core = self.pin_cpus_bg[local_worker_offset];
if !core_affinity::set_for_current(core) {
warn!(
"failed to pin background worker {local_worker_offset} to core {}",
core.id
);
}
}
}
None => {
panic!("pin_cpu() called outside of a runtime or on an aux thread");
}
}
}
fn memory_pressure(&self) -> MemoryPressure {
MemoryPressure::try_from(self.memory_pressure.load(Ordering::Relaxed)).unwrap()
}
fn raw_memory_pressure(&self, process_rss: u64) -> MemoryPressure {
let process_rss = process_rss as f64;
let Some(max_rss) = self.max_rss else {
return MemoryPressure::Low;
};
if process_rss >= (CRITICAL_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
return MemoryPressure::Critical;
}
if process_rss >= (HIGH_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
return MemoryPressure::High;
}
if process_rss >= (MODERATE_MEMORY_PRESSURE_THRESHOLD * max_rss as f64) {
return MemoryPressure::Moderate;
}
MemoryPressure::Low
}
}
fn panic_hook(panic_info: &PanicHookInfo<'_>, default_panic_hook: &dyn Fn(&PanicHookInfo<'_>)) {
default_panic_hook(panic_info);
RUNTIME.with(|runtime| {
if let Ok(runtime) = runtime.try_borrow()
&& let Some(runtime) = runtime.as_ref()
{
runtime.panic(panic_info);
}
})
}
#[repr(transparent)]
#[derive(Clone, Debug)]
pub struct Runtime(Arc<RuntimeInner>);
#[repr(transparent)]
#[derive(Clone, Debug)]
pub struct WeakRuntime(Weak<RuntimeInner>);
impl WeakRuntime {
pub fn upgrade(&self) -> Option<Runtime> {
self.0.upgrade().map(Runtime)
}
}
#[allow(clippy::type_complexity)]
static DEFAULT_PANIC_HOOK: Lazy<Box<dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send>> =
Lazy::new(|| {
let _ = panic::take_hook();
panic::take_hook()
});
fn default_panic_hook() -> &'static (dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send) {
&*DEFAULT_PANIC_HOOK
}
impl Runtime {
pub fn run<F>(config: impl Into<CircuitConfig>, circuit: F) -> Result<RuntimeHandle, DbspError>
where
F: FnOnce(Parker) + Clone + Send + 'static,
{
let mut config: CircuitConfig = config.into();
if config.step_size == StepSize::FullSteps {
config.dev_tweaks.splitter_chunk_size_records = Some(u64::MAX);
}
let workers = config.layout.local_workers();
let num_merger_threads = config.num_merger_threads();
let runtime = Self(Arc::new(RuntimeInner::new(config)?));
let default_hook = default_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
panic_hook(panic_info, default_hook)
}));
let runtime_clone = runtime.clone();
let tokio_merger_runtime: TokioRuntime = {
info!("starting dbsp merger tokio runtime, workers: {num_merger_threads}",);
TokioBuilder::new_multi_thread()
.worker_threads(num_merger_threads)
.thread_name_fn(|| {
use std::sync::atomic::{AtomicUsize, Ordering};
static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
format!("merger-{}", id)
})
.on_thread_start(move || {
set_current_thread_type(ThreadType::Background);
RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime_clone.clone()));
})
.thread_stack_size(6 * 1024 * 1024)
.enable_all()
.build()
.unwrap()
};
runtime
.inner()
.tokio_merger_runtime
.lock()
.unwrap()
.replace(tokio_merger_runtime);
runtime.spawn_aux_thread("rss-monitor", Parker::new(), |parker| {
let runtime = Runtime::runtime().unwrap();
let mut pressure_span = None;
while !Runtime::kill_in_progress() {
let process_rss = process_rss_bytes().unwrap_or_default();
runtime
.inner()
.process_rss
.store(process_rss, Ordering::Relaxed);
let previous_pressure = runtime.inner().memory_pressure();
let current_memory_pressure = runtime.inner().raw_memory_pressure(process_rss);
let new_memory_pressure = match (previous_pressure, current_memory_pressure) {
(MemoryPressure::Low | MemoryPressure::Moderate, _) => current_memory_pressure,
(MemoryPressure::High, MemoryPressure::Critical) => MemoryPressure::Critical,
(MemoryPressure::High, MemoryPressure::High | MemoryPressure::Moderate) => {
MemoryPressure::High
}
(MemoryPressure::High, MemoryPressure::Low) => MemoryPressure::Low,
(MemoryPressure::Critical, MemoryPressure::Critical | MemoryPressure::High) => {
MemoryPressure::Critical
}
(MemoryPressure::Critical, MemoryPressure::Moderate | MemoryPressure::Low) => {
current_memory_pressure
}
};
if pressure_span.is_none() || new_memory_pressure != previous_pressure {
pressure_span = Some(
LongSpanBuilder::new("memory-pressure")
.with_tooltip(new_memory_pressure.to_string())
.build(),
);
}
runtime
.inner()
.memory_pressure
.store(new_memory_pressure as u8, Ordering::Relaxed);
if previous_pressure < MemoryPressure::High
&& new_memory_pressure >= MemoryPressure::High
{
runtime
.inner()
.memory_pressure_epoch
.fetch_add(1, Ordering::Relaxed);
runtime.inner().memory_pressure_notify.notify_waiters();
}
parker.park_timeout(Duration::from_secs(1));
}
drop(pressure_span);
});
let workers = workers
.map(|worker_index| {
let runtime = runtime.clone();
let build_circuit = circuit.clone();
let parker = Parker::new();
let unparker = parker.unparker().clone();
let local_worker_offset =
worker_index - runtime.inner().layout.local_workers().start;
let fbuf_slab_allocator =
runtime.get_fbuf_slab_allocator(local_worker_offset, ThreadType::Foreground);
let handle = Builder::new()
.name(format!("dbsp-worker-{worker_index}"))
.spawn(move || {
WORKER_INDEX.set(worker_index);
set_current_thread_type(ThreadType::Foreground);
set_thread_slab_pool(Some(fbuf_slab_allocator));
runtime.inner().pin_cpu();
RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
build_circuit(parker);
})
.unwrap_or_else(|error| {
panic!("failed to spawn worker thread {worker_index}: {error}");
});
(handle, unparker)
})
.collect::<Vec<_>>();
Ok(RuntimeHandle::new(runtime, workers))
}
pub fn downgrade(&self) -> WeakRuntime {
WeakRuntime(Arc::downgrade(&self.0))
}
#[allow(clippy::self_named_constructors)]
pub fn runtime() -> Option<Runtime> {
RUNTIME.with(|rt| rt.borrow().clone())
}
pub fn storage_backend() -> Result<Arc<dyn StorageBackend>, StorageError> {
Runtime::runtime()
.unwrap()
.inner()
.storage
.as_ref()
.map_or(Err(StorageError::StorageDisabled), |storage| {
Ok(storage.backend.clone())
})
}
pub fn buffer_cache() -> Option<Arc<BufferCache>> {
if let Some(buffer_cache) = BUFFER_CACHE.with(|bc| bc.borrow().clone()) {
Some(buffer_cache)
} else if let Ok(buffer_cache) = TOKIO_BUFFER_CACHE.try_get() {
Some(buffer_cache)
} else if let Some(rt) = Runtime::runtime()
&& let Some(thread_type) = current_thread_type()
{
match thread_type {
ThreadType::Foreground => {
let buffer_cache =
rt.get_buffer_cache(Runtime::local_worker_offset(), thread_type);
BUFFER_CACHE.set(Some(buffer_cache.clone()));
Some(buffer_cache)
}
ThreadType::Background => {
None
}
}
} else {
static AUXILIARY_CACHE: LazyLock<Arc<BufferCache>> =
LazyLock::new(|| Arc::new(BufferCache::new(1024 * 1024 * 256)));
let buffer_cache = AUXILIARY_CACHE.clone();
BUFFER_CACHE.set(Some(buffer_cache.clone()));
Some(buffer_cache)
}
}
pub fn spawn_aux_thread<F>(&self, thread_name: &str, parker: Parker, f: F)
where
F: FnOnce(Parker) + Send + 'static,
{
let runtime = self.clone();
let unparker = parker.unparker().clone();
let handle = Builder::new()
.name(thread_name.to_string())
.spawn(move || {
RUNTIME.with(|rt| *rt.borrow_mut() = Some(runtime));
f(parker)
})
.expect("failed to spawn auxiliary thread");
self.inner()
.aux_threads
.lock()
.unwrap()
.push((handle, unparker))
}
pub fn get_buffer_cache(
&self,
local_worker_offset: usize,
thread_type: ThreadType,
) -> Arc<BufferCache> {
self.0.buffer_caches[local_worker_offset][thread_type].clone()
}
pub(crate) fn get_fbuf_slab_allocator(
&self,
local_worker_offset: usize,
thread_type: ThreadType,
) -> Arc<FBufSlabs> {
self.0.fbuf_slab_allocators[local_worker_offset][thread_type].clone()
}
pub fn cache_occupancy(&self) -> (usize, usize) {
if self.0.storage.is_some() {
let mut seen = HashSet::new();
self.0
.buffer_caches
.iter()
.flat_map(|map| map.values())
.filter(|cache| seen.insert(cache.backend_id()))
.map(|cache| cache.occupancy())
.fold((0, 0), |(a_cur, a_max), (b_cur, b_max)| {
(a_cur + b_cur, a_max + b_max)
})
} else {
(0, 0)
}
}
pub fn fbuf_slabs_stats(&self) -> FBufSlabsStats {
if self.0.storage.is_some() {
let mut seen = HashSet::new();
self.0
.fbuf_slab_allocators
.iter()
.flat_map(|map| map.values())
.filter(|allocator| seen.insert(allocator.backend_id()))
.map(|allocator| allocator.stats())
.fold(FBufSlabsStats::default(), |mut stats, pool_stats| {
stats += pool_stats;
stats
})
} else {
FBufSlabsStats::default()
}
}
pub fn worker_index() -> usize {
match current_thread_type() {
Some(ThreadType::Foreground) => WORKER_INDEX.get(),
Some(ThreadType::Background) => TOKIO_WORKER_INDEX.get(),
None => RUNTIME.with_borrow(|runtime| match runtime {
Some(runtime) => {
runtime.layout().local_workers().start
}
None => {
0
}
}),
}
}
pub fn local_worker_offset() -> usize {
let local_workers_start = RUNTIME
.with(|rt| Some(rt.borrow().as_ref()?.layout().local_workers().start))
.unwrap_or_default();
Self::worker_index() - local_workers_start
}
pub fn mode() -> Mode {
RUNTIME
.with(|rt| Some(rt.borrow().as_ref()?.get_mode()))
.unwrap_or_default()
}
pub fn with_dev_tweaks<F, T>(f: F) -> T
where
F: Fn(&DevTweaks) -> T,
{
static DEFAULT: Lazy<DevTweaks> = Lazy::new(DevTweaks::default);
RUNTIME
.with(|rt| Some(f(&rt.borrow().as_ref()?.inner().dev_tweaks)))
.unwrap_or_else(|| f(&DEFAULT))
}
pub fn get_mode(&self) -> Mode {
self.inner().mode
}
pub fn step_size(&self) -> StepSize {
self.inner().step_size
}
pub fn allow_input_during_commit(&self) -> bool {
self.inner().allow_input_during_commit
}
pub fn worker_index_str() -> &'static str {
static WORKER_INDEX_STRS: Lazy<[&'static str; 256]> = Lazy::new(|| {
let mut data: [&'static str; 256] = [""; 256];
for (i, item) in data.iter_mut().enumerate() {
*item = Box::leak(i.to_string().into_boxed_str());
}
data
});
WORKER_INDEX_STRS
.get(Runtime::worker_index())
.copied()
.unwrap_or_else(|| {
panic!("Limit workers to less than 256 or increase the limit in the code.")
})
}
pub fn memory_pressure() -> Option<MemoryPressure> {
RUNTIME.with(|rt| Some(rt.borrow().as_ref()?.inner().memory_pressure()))
}
pub fn current_memory_pressure(&self) -> MemoryPressure {
self.inner().memory_pressure()
}
pub fn max_rss_bytes(&self) -> Option<u64> {
self.inner().max_rss
}
pub fn memory_pressure_epoch(&self) -> u64 {
self.inner().memory_pressure_epoch.load(Ordering::Relaxed)
}
pub(crate) fn memory_pressure_notify(&self) -> Arc<Notify> {
self.inner().memory_pressure_notify.clone()
}
pub fn min_merge_storage_bytes() -> Option<usize> {
RUNTIME.with(|rt| {
let rt = rt.borrow();
let inner = rt.as_ref()?.inner();
let storage = inner.storage.as_ref()?;
if inner.memory_pressure() >= MemoryPressure::Moderate {
Some(0)
} else {
Some(storage.options.min_storage_bytes.unwrap_or({
10 * 1024 * 1024
}))
}
})
}
pub fn min_insert_storage_bytes() -> Option<usize> {
RUNTIME.with(|rt| {
let rt = rt.borrow();
let inner = rt.as_ref()?.inner();
let storage = inner.storage.as_ref()?;
if inner.memory_pressure() >= MemoryPressure::High {
Some(0)
} else if inner.memory_pressure() >= MemoryPressure::Moderate {
Some(
storage
.options
.min_storage_bytes
.unwrap_or(10 * 1024 * 1024),
)
} else {
Some(usize::MAX)
}
})
}
pub fn min_step_storage_bytes() -> Option<usize> {
RUNTIME.with(|rt| {
let rt = rt.borrow();
let inner = rt.as_ref()?.inner();
let storage = inner.storage.as_ref()?;
if inner.memory_pressure() >= MemoryPressure::Critical {
Some(0)
} else {
Some(storage.options.min_step_storage_bytes.unwrap_or(usize::MAX))
}
})
}
pub fn file_writer_parameters() -> Parameters {
let compression = Runtime::runtime()
.unwrap()
.inner()
.storage
.as_ref()
.unwrap()
.options
.compression;
let compression = match compression {
StorageCompression::Default | StorageCompression::Snappy => Some(Compression::Snappy),
StorageCompression::None => None,
};
Parameters::default().with_compression(compression)
}
fn inner(&self) -> &RuntimeInner {
&self.0
}
pub fn num_workers() -> usize {
RUNTIME.with(|rt| {
rt.borrow()
.as_ref()
.map_or(1, |runtime| runtime.layout().n_workers())
})
}
pub fn layout(&self) -> &Layout {
&self.inner().layout
}
pub fn local_store(&self) -> &LocalStore {
&self.inner().store
}
pub fn storage_path(&self) -> Option<&Path> {
self.inner()
.storage
.as_ref()
.map(|storage| storage.config.path())
}
pub fn sequence_next(&self) -> usize {
self.inner().worker_sequence_numbers[Self::local_worker_offset()]
.fetch_add(1, Ordering::Relaxed)
}
pub fn kill_in_progress() -> bool {
RUNTIME.with(|runtime| {
runtime
.borrow()
.as_ref()
.map(|runtime| runtime.inner().kill_signal.load(Ordering::SeqCst))
.unwrap_or(false)
})
}
pub fn cancellation_token(&self) -> CancellationToken {
self.inner().cancellation_token.clone()
}
pub fn worker_panic_info(
&self,
worker: usize,
thread_type: ThreadType,
) -> Option<WorkerPanicInfo> {
if let Ok(guard) = self.inner().panic_info[worker][thread_type].read() {
guard.clone()
} else {
warn!("poisoned panic_lock lock for {thread_type} worker {worker}");
None
}
}
fn panic(&self, panic_info: &PanicHookInfo) {
let local_worker_offset = Self::local_worker_offset();
let Some(thread_type) = current_thread_type() else {
error!("panic hook called outside of a runtime or on an aux thread");
return;
};
let panic_info = WorkerPanicInfo::new(panic_info);
let _ = self.inner().panic_info[local_worker_offset][thread_type]
.write()
.map(|mut guard| *guard = Some(panic_info));
self.inner().panicked.store(true, Ordering::Release);
}
pub(crate) fn tokio_merger_runtime(&self) -> Option<tokio::runtime::Handle> {
self.inner()
.tokio_merger_runtime
.lock()
.unwrap()
.as_ref()
.map(|rt| rt.handle().clone())
}
pub(crate) fn take_exchange_listener(&self) -> Option<TcpListener> {
self.inner().exchange_listener.lock().unwrap().take()
}
pub fn dev_tweaks(&self) -> &DevTweaks {
&self.inner().dev_tweaks
}
}
pub struct Consensus(Broadcast<bool>);
impl Consensus {
pub fn new(name: impl Display) -> Self {
Self(Broadcast::new(name))
}
pub async fn check(&self, local: bool) -> Result<bool, SchedulerError> {
Ok(self.0.collect(local).await?.into_iter().all(identity))
}
}
pub(crate) enum Broadcast<T> {
SingleThreaded,
MultiThreaded {
exchange: Arc<Exchange<T>>,
identifier: Arc<String>,
},
}
impl<T> Broadcast<T>
where
T: Clone + Debug + Send + serde::Serialize + for<'de> serde::Deserialize<'de> + 'static,
{
pub fn new(name: impl Display) -> Self {
match Runtime::runtime() {
Some(runtime) if Runtime::num_workers() > 1 => {
let exchange_id = runtime.sequence_next().try_into().unwrap();
let exchange =
Exchange::with_runtime(&runtime, exchange_id, ExchangeActivity::AllSteps);
let identifier = Arc::new(format!("broadcast {name} (exchange {exchange_id})"));
Self::MultiThreaded {
exchange,
identifier,
}
}
_ => Self::SingleThreaded,
}
}
pub async fn collect(&self, local: T) -> Result<Vec<T>, SchedulerError> {
match self {
Self::SingleThreaded => Ok(vec![local]),
Self::MultiThreaded {
exchange,
identifier,
} => {
let _span = Span::new("broadcast")
.with_category("Exchange")
.with_tooltip(|| format!("collect for {}", identifier));
Runtime::runtime()
.unwrap()
.cancellation_token()
.run_until_cancelled_owned(async {
exchange
.send_all_with_serializer(identifier, repeat(local.clone()), |local| {
let mut fbuf = FBuf::new();
serde_json::to_writer(&mut fbuf, &local).unwrap();
fbuf
})
.await;
exchange
.receive_all(|data| serde_json::from_slice(&data).unwrap(), None)
.await
.0
})
.await
.ok_or(SchedulerError::Killed)
}
}
}
}
#[derive(Debug)]
pub struct RuntimeHandle {
runtime: Runtime,
workers: Vec<(JoinHandle<()>, Unparker)>,
}
impl RuntimeHandle {
fn new(runtime: Runtime, workers: Vec<(JoinHandle<()>, Unparker)>) -> Self {
Self { runtime, workers }
}
pub(super) fn unpark_worker(&self, worker: usize) {
self.workers[worker].1.unpark();
}
pub fn runtime(&self) -> &Runtime {
&self.runtime
}
pub fn kill(self) -> ThreadResult<()> {
self.kill_async();
self.join()
}
pub fn kill_async(&self) {
self.runtime
.inner()
.kill_signal
.store(true, Ordering::SeqCst);
self.runtime.inner().cancellation_token.cancel();
for (_worker, unparker) in self.workers.iter() {
unparker.unpark();
}
self.runtime
.inner()
.aux_threads
.lock()
.unwrap()
.iter()
.for_each(|(_h, unparker)| {
unparker.unpark();
});
}
pub fn join(self) -> ThreadResult<()> {
#[allow(clippy::needless_collect)]
let results: Vec<ThreadResult<()>> = self
.workers
.into_iter()
.map(|(h, _unparker)| h.join())
.collect();
self.runtime
.inner()
.kill_signal
.store(true, Ordering::SeqCst);
self.runtime
.inner()
.aux_threads
.lock()
.unwrap()
.iter()
.for_each(|(_h, unparker)| {
unparker.unpark();
});
self.runtime
.inner()
.aux_threads
.lock()
.unwrap()
.drain(..)
.for_each(|(h, _unparker)| {
let _ = h.join();
});
let tokio_merger_runtime = self
.runtime
.inner()
.tokio_merger_runtime
.lock()
.unwrap()
.take();
drop(tokio_merger_runtime);
self.runtime.local_store().clear();
results.into_iter().collect()
}
pub fn worker_panic_info(
&self,
worker: usize,
thread_type: ThreadType,
) -> Option<WorkerPanicInfo> {
self.runtime.worker_panic_info(worker, thread_type)
}
pub fn collect_panic_info(&self) -> Vec<(usize, ThreadType, WorkerPanicInfo)> {
let mut result = Vec::new();
for worker in 0..self.workers.len() {
for thread_type in [ThreadType::Foreground, ThreadType::Background] {
if let Some(panic_info) = self.worker_panic_info(worker, thread_type) {
result.push((worker, thread_type, panic_info))
}
}
}
result
}
pub fn panicked(&self) -> bool {
self.runtime.inner().panicked.load(Ordering::Acquire)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WorkerLocation {
Local,
Remote,
}
#[derive(Clone, Debug)]
pub struct WorkerLocations {
workers: Range<usize>,
local_workers: Range<usize>,
}
impl Default for WorkerLocations {
fn default() -> Self {
Self::new()
}
}
impl WorkerLocations {
pub fn new() -> Self {
if let Some(runtime) = Runtime::runtime() {
Self::for_layout(runtime.layout())
} else {
Self {
workers: 0..1,
local_workers: 0..1,
}
}
}
pub fn for_layout(layout: &Layout) -> Self {
Self {
workers: 0..layout.n_workers(),
local_workers: layout.local_workers(),
}
}
pub fn get(&self, worker: usize) -> WorkerLocation {
self[worker]
}
}
impl Index<usize> for WorkerLocations {
type Output = WorkerLocation;
fn index(&self, worker: usize) -> &Self::Output {
debug_assert!(worker < self.workers.end);
if self.local_workers.contains(&worker) {
&WorkerLocation::Local
} else {
&WorkerLocation::Remote
}
}
}
impl Iterator for WorkerLocations {
type Item = WorkerLocation;
fn next(&mut self) -> Option<Self::Item> {
let worker = self.workers.next()?;
Some(self.get(worker))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.workers.len();
(len, Some(len))
}
}
impl ExactSizeIterator for WorkerLocations {}
#[cfg(test)]
mod tests {
use super::{Runtime, RuntimeInner};
use crate::{
Circuit, RootCircuit,
circuit::{
CircuitConfig,
dbsp_handle::CircuitStorageConfig,
metadata::{LOOSE_MEMORY_RECORDS_COUNT, MERGING_MEMORY_RECORDS_COUNT},
schedule::{DynamicScheduler, Scheduler},
},
operator::Generator,
profile::DbspProfile,
storage::backend::FileId,
};
use enum_map::Enum;
use feldera_buffer_cache::{CacheEntry, ThreadType};
use feldera_storage::fbuf::{FBuf, slab::set_thread_slab_pool};
use feldera_types::config::{
StorageCacheConfig, StorageConfig, StorageOptions,
dev_tweaks::{BufferCacheAllocationStrategy, BufferCacheStrategy},
};
use feldera_types::memory_pressure::MemoryPressure;
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, LazyLock, Mutex},
thread::sleep,
time::{Duration, Instant},
};
struct TestCacheEntry(usize);
impl CacheEntry for TestCacheEntry {
fn cost(&self) -> usize {
self.0
}
}
static MOCK_RSS_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
fn set_mock_process_rss_bytes(bytes: u64) {
unsafe {
std::env::set_var("MOCK_PROCESS_RSS_BYTES", bytes.to_string());
}
}
struct MockRssVarGuard;
impl Drop for MockRssVarGuard {
fn drop(&mut self) {
unsafe {
std::env::remove_var("MOCK_PROCESS_RSS_BYTES");
}
}
}
fn query_runtime_memory_state(runtime: &Runtime) -> (MemoryPressure, usize, usize, usize) {
let (sender, receiver) = std::sync::mpsc::channel();
runtime.spawn_aux_thread(
"memory-pressure-test-query",
super::Parker::new(),
move |_parker| {
let _ = sender.send((
Runtime::memory_pressure().expect("query thread should run inside runtime"),
Runtime::min_merge_storage_bytes().expect("runtime has storage configured"),
Runtime::min_insert_storage_bytes().expect("runtime has storage configured"),
Runtime::min_step_storage_bytes().expect("runtime has storage configured"),
));
},
);
receiver
.recv_timeout(Duration::from_secs(5))
.expect("failed to query memory state from runtime thread")
}
fn count_metric(
profile: &DbspProfile,
metric: &'static crate::circuit::metadata::MetricId,
) -> usize {
let root_node = crate::circuit::GlobalNodeId::root();
profile
.worker_profiles
.iter()
.map(|worker| {
worker
.get_node_profile(&root_node)
.map(|meta| {
meta.iter()
.filter_map(|((metric_id, _labels), value)| {
if metric_id == metric {
match value {
crate::circuit::metadata::MetaItem::Count(count) => {
Some(*count)
}
_ => None,
}
} else {
None
}
})
.sum::<usize>()
})
.unwrap_or(0)
})
.sum()
}
fn in_memory_spine_records(profile: &DbspProfile) -> usize {
count_metric(profile, &LOOSE_MEMORY_RECORDS_COUNT)
+ count_metric(profile, &MERGING_MEMORY_RECORDS_COUNT)
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_runtime_dynamic() {
test_runtime::<DynamicScheduler>();
}
#[test]
#[cfg_attr(miri, ignore)]
fn storage_no_cleanup() {
let path = tempfile::tempdir().unwrap().keep();
let path_clone = path.clone();
let cconf = CircuitConfig::with_workers(4).with_storage(Some(
CircuitStorageConfig::for_config(
StorageConfig {
path: path.to_string_lossy().into_owned(),
cache: StorageCacheConfig::default(),
},
StorageOptions::default(),
)
.unwrap(),
));
let hruntime = Runtime::run(cconf, move |_parker| {
let runtime = Runtime::runtime().unwrap();
assert_eq!(runtime.storage_path(), Some(path_clone.as_ref()));
})
.expect("failed to start runtime");
hruntime.join().unwrap();
assert!(path.exists(), "persistent storage is not cleaned up");
}
#[test]
fn s3_fifo_is_the_default_buffer_cache_strategy() {
let inner = RuntimeInner::new(CircuitConfig::with_workers(1)).unwrap();
assert_eq!(
inner.buffer_caches[0][ThreadType::Foreground].strategy(),
BufferCacheStrategy::S3Fifo
);
}
#[test]
fn default_s3_fifo_cache_shares_backend_per_worker_pair() {
let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
assert!(
inner.buffer_caches[0][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
);
assert!(
inner.buffer_caches[1][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
);
assert!(
!inner.buffer_caches[0][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground])
);
}
#[test]
fn lru_can_still_be_selected_explicitly() {
let inner = RuntimeInner::new(
CircuitConfig::with_workers(1).with_buffer_cache_strategy(BufferCacheStrategy::Lru),
)
.unwrap();
assert_eq!(
inner.buffer_caches[0][ThreadType::Foreground].strategy(),
BufferCacheStrategy::Lru
);
assert!(
!inner.buffer_caches[0][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
);
}
#[test]
fn lru_uses_default_total_cache_capacity_when_cache_mib_is_unset() {
let workers = 3usize;
let path = tempfile::tempdir().unwrap();
let storage = CircuitStorageConfig::for_config(
StorageConfig {
path: path.path().to_string_lossy().into_owned(),
cache: StorageCacheConfig::default(),
},
StorageOptions::default(),
)
.unwrap();
let runtime = Runtime(Arc::new(
RuntimeInner::new(
CircuitConfig::with_workers(workers)
.with_storage(Some(storage))
.with_buffer_cache_strategy(BufferCacheStrategy::Lru),
)
.unwrap(),
));
assert_eq!(
runtime.cache_occupancy(),
(0, workers * ThreadType::LENGTH * 256usize * 1024 * 1024)
);
}
#[test]
fn s3_fifo_cache_can_share_cache_per_worker_pair_when_requested() {
let config = CircuitConfig::with_workers(2).with_buffer_cache_allocation_strategy(
BufferCacheAllocationStrategy::SharedPerWorkerPair,
);
let inner = RuntimeInner::new(config).unwrap();
assert!(
inner.buffer_caches[0][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
);
assert!(
inner.buffer_caches[1][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
);
}
#[test]
fn fbuf_slabs_are_shared_per_worker_pair() {
let inner = RuntimeInner::new(CircuitConfig::with_workers(2)).unwrap();
assert!(Arc::ptr_eq(
&inner.fbuf_slab_allocators[0][ThreadType::Foreground],
&inner.fbuf_slab_allocators[0][ThreadType::Background],
));
assert!(Arc::ptr_eq(
&inner.fbuf_slab_allocators[1][ThreadType::Foreground],
&inner.fbuf_slab_allocators[1][ThreadType::Background],
));
assert!(!Arc::ptr_eq(
&inner.fbuf_slab_allocators[0][ThreadType::Foreground],
&inner.fbuf_slab_allocators[1][ThreadType::Foreground],
));
}
#[test]
fn fbuf_slabs_honor_bytes_per_class_dev_tweak() {
let inner =
RuntimeInner::new(CircuitConfig::with_workers(1).with_fbuf_slab_bytes_per_class(4096))
.unwrap();
let allocator = inner.fbuf_slab_allocators[0][ThreadType::Foreground].clone();
let previous = set_thread_slab_pool(Some(allocator.clone()));
let first = FBuf::with_capacity(4096);
let second = FBuf::with_capacity(4096);
drop(first);
drop(second);
set_thread_slab_pool(previous);
assert_eq!(allocator.stats().cached_buffers, 1);
}
#[test]
fn sieve_can_share_one_global_cache_when_requested() {
let config = CircuitConfig::with_workers(2)
.with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global);
let inner = RuntimeInner::new(config).unwrap();
let global = inner.buffer_caches[0][ThreadType::Foreground].clone();
assert!(global.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background]));
assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Foreground]));
assert!(global.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background]));
}
#[test]
fn lru_keeps_separate_foreground_and_background_caches() {
let config = CircuitConfig::with_workers(2)
.with_buffer_cache_strategy(BufferCacheStrategy::Lru)
.with_buffer_cache_allocation_strategy(
BufferCacheAllocationStrategy::SharedPerWorkerPair,
);
let inner = RuntimeInner::new(config).unwrap();
assert!(
!inner.buffer_caches[0][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[0][ThreadType::Background])
);
assert!(
!inner.buffer_caches[1][ThreadType::Foreground]
.shares_backend_with(&inner.buffer_caches[1][ThreadType::Background])
);
}
#[test]
fn shared_sharded_cache_occupancy_is_not_double_counted() {
let path = tempfile::tempdir().unwrap();
let storage = CircuitStorageConfig::for_config(
StorageConfig {
path: path.path().to_string_lossy().into_owned(),
cache: StorageCacheConfig::default(),
},
StorageOptions {
cache_mib: Some(8),
..StorageOptions::default()
},
)
.unwrap();
let runtime = Runtime(Arc::new(
RuntimeInner::new(
CircuitConfig::with_workers(1)
.with_storage(Some(storage))
.with_buffer_cache_allocation_strategy(
BufferCacheAllocationStrategy::SharedPerWorkerPair,
),
)
.unwrap(),
));
runtime.get_buffer_cache(0, ThreadType::Foreground).insert(
FileId::new(),
0,
Arc::new(TestCacheEntry(1024)),
);
assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
}
#[test]
fn global_sharded_cache_occupancy_is_not_double_counted() {
let path = tempfile::tempdir().unwrap();
let storage = CircuitStorageConfig::for_config(
StorageConfig {
path: path.path().to_string_lossy().into_owned(),
cache: StorageCacheConfig::default(),
},
StorageOptions {
cache_mib: Some(8),
..StorageOptions::default()
},
)
.unwrap();
let runtime = Runtime(Arc::new(
RuntimeInner::new(
CircuitConfig::with_workers(2)
.with_storage(Some(storage))
.with_buffer_cache_allocation_strategy(BufferCacheAllocationStrategy::Global),
)
.unwrap(),
));
runtime.get_buffer_cache(1, ThreadType::Background).insert(
FileId::new(),
0,
Arc::new(TestCacheEntry(1024)),
);
assert_eq!(runtime.cache_occupancy(), (1024, 8 * 1024 * 1024));
}
#[test]
fn shared_fbuf_slab_stats_are_not_double_counted() {
let path = tempfile::tempdir().unwrap();
let storage = CircuitStorageConfig::for_config(
StorageConfig {
path: path.path().to_string_lossy().into_owned(),
cache: StorageCacheConfig::default(),
},
StorageOptions::default(),
)
.unwrap();
let runtime = Runtime(Arc::new(
RuntimeInner::new(CircuitConfig::with_workers(1).with_storage(Some(storage))).unwrap(),
));
let previous = set_thread_slab_pool(Some(
runtime.get_fbuf_slab_allocator(0, ThreadType::Foreground),
));
let first = FBuf::with_capacity(4096);
drop(first);
let second = FBuf::with_capacity(3000);
drop(second);
set_thread_slab_pool(previous);
let stats = runtime.fbuf_slabs_stats();
let class = stats
.classes
.iter()
.find(|class| class.size == 4096)
.unwrap();
assert_eq!(stats.alloc_requests(), 2);
assert_eq!(stats.mallocs_saved(), 1);
assert_eq!(stats.frees_saved(), 2);
assert_eq!(stats.cached_buffers, 1);
assert_eq!(class.alloc_requests, 2);
assert_eq!(class.alloc_hits, 1);
assert_eq!(class.recycle_requests, 2);
assert_eq!(class.recycle_hits, 2);
}
fn test_runtime<S>()
where
S: Scheduler + 'static,
{
let hruntime = Runtime::run(4, |_parker| {
let data = Rc::new(RefCell::new(vec![]));
let data_clone = data.clone();
let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
let runtime = Runtime::runtime().unwrap();
circuit
.add_source(Generator::new(move || runtime.sequence_next()))
.inspect(move |n: &usize| data_clone.borrow_mut().push(*n));
Ok(())
})
.unwrap()
.0;
for _ in 0..100 {
root.transaction().unwrap();
}
assert_eq!(&*data.borrow(), &(2..102).collect::<Vec<usize>>());
})
.expect("failed to start runtime");
hruntime.join().unwrap();
}
#[test]
fn test_kill_dynamic() {
test_kill::<DynamicScheduler>();
}
fn test_kill<S>()
where
S: Scheduler + 'static,
{
let hruntime = Runtime::run(16, |_parker| {
let root = RootCircuit::build_with_scheduler::<_, _, S>(move |circuit| {
circuit
.iterate_with_scheduler::<_, _, _, S>(|child| {
let mut n: usize = 0;
child
.add_source(Generator::new(move || {
n += 1;
n
}))
.inspect(|_: &usize| {});
Ok((async || Ok(false), ()))
})
.unwrap();
Ok(())
})
.unwrap()
.0;
loop {
if root.transaction().is_err() {
return;
}
}
})
.expect("failed to start runtime");
sleep(Duration::from_millis(100));
hruntime.kill().unwrap();
}
#[test]
fn memory_pressure_thresholds_and_spill_behavior() {
const GIB: u64 = 1024 * 1024 * 1024;
const MIB: u64 = 1024 * 1024;
const TEN_MIB: usize = 10 * 1024 * 1024;
let _mock_guard = MOCK_RSS_LOCK.lock().unwrap();
let _clear_mock_rss = MockRssVarGuard;
set_mock_process_rss_bytes(0);
let storage_dir = tempfile::tempdir().unwrap();
let config = CircuitConfig::with_workers(1)
.with_temporary_storage(storage_dir.path())
.with_max_rss_bytes(Some(10 * GIB));
let (mut circuit, input_handle) = Runtime::init_circuit(config, move |circuit| {
let (stream, input_handle) = circuit.add_input_zset::<u64>();
stream.accumulate_integrate_trace();
Ok(input_handle)
})
.unwrap();
for key in 0..7 {
input_handle.push(key, 1);
circuit.transaction().unwrap();
}
let (pressure, min_merge, min_insert, min_step) =
query_runtime_memory_state(circuit.runtime());
assert_eq!(pressure, MemoryPressure::Low);
assert_eq!(min_merge, TEN_MIB);
assert_eq!(min_insert, usize::MAX);
assert_eq!(min_step, usize::MAX);
let profile = circuit.retrieve_profile().unwrap();
assert!(in_memory_spine_records(&profile) == 7);
set_mock_process_rss_bytes(8800 * MIB);
sleep(Duration::from_secs(5));
let (pressure, min_merge, min_insert, min_step) =
query_runtime_memory_state(circuit.runtime());
assert_eq!(pressure, MemoryPressure::Moderate);
assert_eq!(min_merge, 0);
assert_eq!(min_insert, TEN_MIB);
assert_eq!(min_step, usize::MAX);
set_mock_process_rss_bytes(9400 * MIB);
sleep(Duration::from_secs(5));
let (pressure, min_merge, min_insert, min_step) =
query_runtime_memory_state(circuit.runtime());
assert_eq!(pressure, MemoryPressure::High);
assert_eq!(min_merge, 0);
assert_eq!(min_insert, 0);
assert_eq!(min_step, usize::MAX);
let deadline = Instant::now() + Duration::from_secs(20);
loop {
let profile = circuit.retrieve_profile().unwrap();
if in_memory_spine_records(&profile) == 0 {
break;
}
assert!(
Instant::now() < deadline,
"in-memory spine records did not drain to zero under high memory pressure"
);
sleep(Duration::from_millis(250));
}
set_mock_process_rss_bytes(9800 * MIB);
sleep(Duration::from_secs(3));
let (pressure, min_merge, min_insert, min_step) =
query_runtime_memory_state(circuit.runtime());
assert_eq!(pressure, MemoryPressure::Critical);
assert_eq!(min_merge, 0);
assert_eq!(min_insert, 0);
assert_eq!(min_step, 0);
circuit.kill().unwrap();
}
}