use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::{Error, Result};
pub const DEFAULT_SPILL_THRESHOLD_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Clone, Debug)]
pub enum SpillPolicy {
FailFast,
SpillToDisk {
directory: PathBuf,
},
}
pub trait SpillMetricsSink: Send + Sync {
fn record_spill(&self, bytes: u64, files: u64);
}
pub trait ByteSized {
fn estimated_bytes(&self) -> u64;
}
#[derive(Clone)]
pub struct MemoryPolicy {
limit_bytes: Option<u64>,
spill_policy: SpillPolicy,
metrics: Option<Arc<dyn SpillMetricsSink>>,
}
impl MemoryPolicy {
pub fn new(limit_bytes: Option<u64>, spill_policy: SpillPolicy) -> Self {
Self {
limit_bytes,
spill_policy,
metrics: None,
}
}
pub fn limit_bytes(&self) -> Option<u64> {
self.limit_bytes
}
pub fn spill_policy(&self) -> &SpillPolicy {
&self.spill_policy
}
pub fn with_metrics(mut self, metrics: Arc<dyn SpillMetricsSink>) -> Self {
self.metrics = Some(metrics);
self
}
pub fn spill_directory(&self) -> Option<&Path> {
match &self.spill_policy {
SpillPolicy::SpillToDisk { directory } => Some(directory.as_path()),
SpillPolicy::FailFast => None,
}
}
pub fn record_spill(&self, bytes: u64, files: u64) {
if let Some(metrics) = &self.metrics {
metrics.record_spill(bytes, files);
}
}
pub fn over_limit(&self, used_bytes: u64) -> bool {
self.limit_bytes
.map(|limit| used_bytes > limit)
.unwrap_or(false)
}
pub fn enforce(&self, used_bytes: u64) -> Result<()> {
let Some(limit) = self.limit_bytes else {
return Ok(());
};
if used_bytes <= limit {
return Ok(());
}
match &self.spill_policy {
SpillPolicy::FailFast => Err(Error::MemoryLimitExceeded {
limit: saturating_usize(limit),
requested: saturating_usize(used_bytes),
}),
SpillPolicy::SpillToDisk { .. } => Ok(()),
}
}
}
impl std::fmt::Debug for MemoryPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryPolicy")
.field("limit_bytes", &self.limit_bytes)
.field("spill_policy", &self.spill_policy)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct MemoryTracker {
policy: MemoryPolicy,
used_bytes: u64,
}
impl MemoryTracker {
pub fn new(policy: MemoryPolicy) -> Self {
Self {
policy,
used_bytes: 0,
}
}
pub fn used_bytes(&self) -> u64 {
self.used_bytes
}
pub fn policy(&self) -> &MemoryPolicy {
&self.policy
}
pub fn over_limit(&self) -> bool {
self.policy.over_limit(self.used_bytes)
}
pub fn reset(&mut self) {
self.used_bytes = 0;
}
pub fn add_bytes(&mut self, bytes: u64) -> Result<()> {
self.used_bytes = self.used_bytes.saturating_add(bytes);
self.policy.enforce(self.used_bytes)
}
pub fn add_row<T: ByteSized>(&mut self, values: &[T]) -> Result<()> {
self.add_values(values)
}
pub fn add_values<T: ByteSized>(&mut self, values: &[T]) -> Result<()> {
let bytes: u64 = values.iter().map(ByteSized::estimated_bytes).sum();
self.add_bytes(bytes)
}
pub fn add_value<T: ByteSized>(&mut self, value: &T) -> Result<()> {
self.add_bytes(value.estimated_bytes())
}
}
fn saturating_usize(value: u64) -> usize {
usize::try_from(value).unwrap_or(usize::MAX)
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::{ByteSized, MemoryPolicy, MemoryTracker, SpillPolicy};
use crate::Error;
#[derive(Clone)]
struct TestValue(u64);
impl ByteSized for TestValue {
fn estimated_bytes(&self) -> u64 {
self.0
}
}
#[test]
fn memory_tracker_detects_fail_fast_limit_exceeded() {
let policy = MemoryPolicy::new(Some(10), SpillPolicy::FailFast);
let mut tracker = MemoryTracker::new(policy);
tracker.add_value(&TestValue(6)).unwrap();
let err = tracker.add_value(&TestValue(5)).unwrap_err();
assert!(tracker.over_limit());
assert_eq!(tracker.used_bytes(), 11);
assert!(matches!(
err,
Error::MemoryLimitExceeded {
limit: 10,
requested: 11
}
));
}
#[test]
fn spill_to_disk_policy_allows_limit_exceeded_until_caller_spills() {
let dir = tempfile::tempdir().unwrap();
let policy = MemoryPolicy::new(
Some(4),
SpillPolicy::SpillToDisk {
directory: dir.path().to_path_buf(),
},
);
let mut tracker = MemoryTracker::new(policy);
tracker.add_values(&[TestValue(2), TestValue(3)]).unwrap();
assert!(tracker.over_limit());
assert_eq!(tracker.used_bytes(), 5);
tracker.reset();
assert_eq!(tracker.used_bytes(), 0);
}
}