use super::memory::BatchGuard;
use super::{ExecutionContext, MemoryBudgetError, MemoryReservation};
use std::sync::{Arc, Mutex};
pub trait Batch {
fn rows(&self) -> usize;
fn retained_bytes(&self) -> usize;
fn is_empty(&self) -> bool {
self.rows() == 0
}
}
pub struct BatchLease<B: Batch> {
batch: LeaseBatch<B>,
batch_guard: Option<BatchGuard>,
reservation: Option<MemoryReservation>,
recycler: Option<Arc<BatchBufferPool<B>>>,
}
enum LeaseBatch<B> {
Owned(B),
Recyclable(Arc<B>),
}
impl<B: Batch> BatchLease<B> {
pub fn try_new(batch: B, context: &ExecutionContext) -> Result<Self, MemoryBudgetError> {
let reservation = context.try_reserve(batch.retained_bytes())?;
let batch_guard = reservation.begin_batch();
Ok(Self {
batch: LeaseBatch::Owned(batch),
batch_guard: Some(batch_guard),
reservation: Some(reservation),
recycler: None,
})
}
pub fn try_from_reservation(
batch: B,
mut reservation: MemoryReservation,
) -> Result<Self, MemoryBudgetError> {
let retained = batch.retained_bytes();
if retained > reservation.bytes() {
drop(batch);
return Err(MemoryBudgetError::Exhausted {
requested: retained,
available: reservation.bytes(),
});
}
reservation.shrink_to(retained);
let batch_guard = reservation.begin_batch();
Ok(Self {
batch: LeaseBatch::Owned(batch),
batch_guard: Some(batch_guard),
reservation: Some(reservation),
recycler: None,
})
}
pub fn try_from_reservation_recycling(
batch: B,
mut reservation: MemoryReservation,
recycler: Arc<BatchBufferPool<B>>,
) -> Result<Self, MemoryBudgetError> {
let retained = batch.retained_bytes();
if retained > reservation.bytes() {
drop(batch);
return Err(MemoryBudgetError::Exhausted {
requested: retained,
available: reservation.bytes(),
});
}
reservation.shrink_to(retained);
let batch_guard = reservation.begin_batch();
Ok(Self {
batch: LeaseBatch::Recyclable(Arc::new(batch)),
batch_guard: Some(batch_guard),
reservation: Some(reservation),
recycler: Some(recycler),
})
}
#[must_use]
pub fn batch(&self) -> &B {
match &self.batch {
LeaseBatch::Owned(batch) => batch,
LeaseBatch::Recyclable(batch) => batch,
}
}
}
impl<B: Batch> Drop for BatchLease<B> {
fn drop(&mut self) {
drop(self.batch_guard.take());
let Some(recycler) = &self.recycler else {
return;
};
let Some(reservation) = self.reservation.take() else {
return;
};
let LeaseBatch::Recyclable(batch) = &self.batch else {
return;
};
self.reservation = recycler.put_or_return(Arc::clone(batch), reservation);
}
}
impl<B: Batch + std::fmt::Debug> std::fmt::Debug for BatchLease<B> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("BatchLease")
.field("batch", self.batch())
.field("recycling", &self.recycler.is_some())
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct BatchBufferPool<B: Batch> {
slot: Mutex<Option<(Arc<B>, MemoryReservation)>>,
}
impl<B: Batch> BatchBufferPool<B> {
#[must_use]
pub const fn new() -> Self {
Self {
slot: Mutex::new(None),
}
}
pub fn take(&self) -> Option<(B, MemoryReservation)> {
let Ok(mut slot) = self.slot.lock() else {
return None;
};
if slot
.as_ref()
.is_some_and(|(batch, _)| Arc::strong_count(batch) != 1)
{
return None;
}
let (batch, reservation) = slot.take()?;
match Arc::try_unwrap(batch) {
Ok(batch) => Some((batch, reservation)),
Err(batch) => {
*slot = Some((batch, reservation));
None
}
}
}
pub fn put(&self, batch: B, reservation: MemoryReservation) -> Result<(), MemoryBudgetError> {
let retained = batch.retained_bytes();
if retained > reservation.bytes() {
drop(batch);
return Err(MemoryBudgetError::Exhausted {
requested: retained,
available: reservation.bytes(),
});
}
drop(self.put_or_return(Arc::new(batch), reservation));
Ok(())
}
fn put_or_return(
&self,
batch: Arc<B>,
reservation: MemoryReservation,
) -> Option<MemoryReservation> {
if let Ok(mut slot) = self.slot.lock()
&& slot.is_none()
{
*slot = Some((batch, reservation));
return None;
}
drop(batch);
Some(reservation)
}
}
impl<B: Batch> Default for BatchBufferPool<B> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BatchDemand {
pub max_rows: usize,
pub max_bytes: usize,
}
impl BatchDemand {
#[must_use]
pub const fn new(max_rows: usize, max_bytes: usize) -> Self {
Self {
max_rows,
max_bytes,
}
}
#[must_use]
pub const fn can_accept_work(self) -> bool {
self.max_rows > 0 && self.max_bytes > 0
}
#[must_use]
pub fn accepts<B: Batch>(self, batch: &B) -> bool {
batch.rows() <= self.max_rows && batch.retained_bytes() <= self.max_bytes
}
}
#[derive(Debug)]
pub enum Backpressure<B> {
Ready(B),
Pending,
Finished,
}
pub trait BatchSource {
type Batch: Batch;
type Error;
fn next_batch(
&mut self,
demand: BatchDemand,
context: &ExecutionContext,
) -> Result<Backpressure<BatchLease<Self::Batch>>, Self::Error>;
}
#[cfg(test)]
#[path = "batch_tests.rs"]
mod tests;