use core::time::Duration;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BackpressureThresholds {
pub l0_slowdown: Option<usize>,
pub l0_stop: Option<usize>,
pub bytes_slowdown: Option<u64>,
pub bytes_stop: Option<u64>,
pub memtable_slowdown: Option<u64>,
pub memtable_stop: Option<u64>,
pub max_slowdown: Option<Duration>,
}
impl BackpressureThresholds {
pub const OFF: Self = Self {
l0_slowdown: None,
l0_stop: None,
bytes_slowdown: None,
bytes_stop: None,
memtable_slowdown: None,
memtable_stop: None,
max_slowdown: None,
};
#[must_use]
pub const fn is_off(&self) -> bool {
self.l0_slowdown.is_none()
&& self.l0_stop.is_none()
&& self.bytes_slowdown.is_none()
&& self.bytes_stop.is_none()
&& self.memtable_slowdown.is_none()
&& self.memtable_stop.is_none()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BackpressureSignals {
pub l0_runs: usize,
pub pending_compaction_bytes: u64,
pub unflushed_memtable_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backpressure {
None,
Slowdown {
suggested_delay: Duration,
},
Stop,
}
impl Backpressure {
#[must_use]
pub fn compute(signals: &BackpressureSignals, thresholds: &BackpressureThresholds) -> Self {
if thresholds.is_off() {
return Self::None;
}
let BackpressureSignals {
l0_runs,
pending_compaction_bytes: pending,
unflushed_memtable_bytes: unflushed,
} = *signals;
if thresholds.l0_stop.is_some_and(|t| l0_runs >= t)
|| thresholds.bytes_stop.is_some_and(|t| pending >= t)
|| thresholds.memtable_stop.is_some_and(|t| unflushed >= t)
{
return Self::Stop;
}
let cap = thresholds.max_slowdown.unwrap_or(Duration::ZERO);
let mut delay = Duration::ZERO;
let mut slowing = false;
if let Some(soft) = thresholds.l0_slowdown
&& l0_runs >= soft
{
slowing = true;
delay = delay.max(ramp_usize(l0_runs, soft, thresholds.l0_stop, cap));
}
if let Some(soft) = thresholds.bytes_slowdown
&& pending >= soft
{
slowing = true;
delay = delay.max(ramp_u64(pending, soft, thresholds.bytes_stop, cap));
}
if let Some(soft) = thresholds.memtable_slowdown
&& unflushed >= soft
{
slowing = true;
delay = delay.max(ramp_u64(unflushed, soft, thresholds.memtable_stop, cap));
}
if slowing {
Self::Slowdown {
suggested_delay: delay,
}
} else {
Self::None
}
}
#[must_use]
pub const fn is_throttled(&self) -> bool {
!matches!(self, Self::None)
}
}
#[cfg(feature = "std")]
type MemtableProbe = alloc::sync::Weak<arc_swap::ArcSwap<crate::version::SuperVersion>>;
#[cfg(feature = "std")]
#[derive(Default)]
pub struct WriteBufferBudget {
members: parking_lot::Mutex<alloc::vec::Vec<MemtableProbe>>,
}
#[cfg(feature = "std")]
impl WriteBufferBudget {
#[must_use]
pub fn unflushed_bytes(&self) -> u64 {
let mut members = self.members.lock();
let mut total = 0u64;
members.retain(|member| {
let Some(mirror) = member.upgrade() else {
return false;
};
total += mirror.load().unflushed_bytes();
true
});
total
}
pub(crate) fn register(&self, mirror: MemtableProbe) {
self.members.lock().push(mirror);
}
}
#[cfg(feature = "std")]
impl core::fmt::Debug for WriteBufferBudget {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("WriteBufferBudget")
.field("members", &self.members.lock().len())
.finish()
}
}
fn ramp_usize(value: usize, soft: usize, hard: Option<usize>, cap: Duration) -> Duration {
match hard {
Some(hard) if hard > soft => {
let num = (value - soft) as u64;
let den = (hard - soft) as u64;
scale(cap, num, den)
}
_ => cap,
}
}
fn ramp_u64(value: u64, soft: u64, hard: Option<u64>, cap: Duration) -> Duration {
match hard {
Some(hard) if hard > soft => {
let num = value - soft;
let den = hard - soft;
scale(cap, num, den)
}
_ => cap,
}
}
fn scale(cap: Duration, num: u64, den: u64) -> Duration {
if den == 0 {
return cap;
}
let cap_nanos = cap.as_nanos();
let num = u128::from(num);
let den = u128::from(den);
let nanos = (cap_nanos / den) * num + ((cap_nanos % den) * num) / den;
let secs = u64::try_from(nanos / 1_000_000_000).unwrap_or(u64::MAX);
let sub = (nanos % 1_000_000_000) as u32;
Duration::new(secs, sub)
}
#[cfg(test)]
mod tests;