Skip to content

Commit 6adf996

Browse files
committed
[MemoryPool] Add support for overdrafts in MemoryPool
This PR adds support for overdrafts in `MemoryPool` via the `force_reserve` API. This allows the memory pool to issue leases for more capacity than it holds. It also introduces a new `overdraft` API to query how much in the negative the memory pool is. Also, it introduces a new `wait_until_available` which waits until the pool is out of the overdraft mode (just as a notification without reserving anything).
1 parent 63c65e8 commit 6adf996

2 files changed

Lines changed: 149 additions & 1 deletion

File tree

crates/memory/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ tokio = { workspace = true, features = ["sync"] }
2525
tracing = { workspace = true }
2626

2727
[dev-dependencies]
28-
tokio = { workspace = true, features = ["rt", "macros", "time"] }
28+
tokio = { workspace = true, features = ["rt", "macros", "time", "test-util"] }
2929

3030
[lints]
3131
workspace = true

crates/memory/src/pool.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ impl MemoryPool {
108108
}
109109
}
110110

111+
/// Returns the number of bytes in overdraft (used - capacity). Returns zero if
112+
/// usage is still within capacity.
113+
#[inline]
114+
pub fn overdraft(&self) -> usize {
115+
match &self.inner {
116+
Some(inner) => {
117+
let capacity = inner.capacity.load(Ordering::Relaxed);
118+
let used = inner.used.load(Ordering::Relaxed);
119+
used.saturating_sub(capacity)
120+
}
121+
None => 0,
122+
}
123+
}
124+
111125
/// Tries to reserve `size` bytes without waiting.
112126
///
113127
/// Returns `None` if insufficient capacity.
@@ -172,6 +186,59 @@ impl MemoryPool {
172186
}
173187
}
174188

189+
/// Waits until there's any available budget. There's no guarantees
190+
/// though that by the time the caller is woken up, that the budget
191+
/// will still be available.
192+
pub async fn wait_until_available(&self) {
193+
let Some(inner) = &self.inner else {
194+
return;
195+
};
196+
loop {
197+
let notified = inner.notify.notified();
198+
if self.available() > 0 {
199+
break;
200+
}
201+
notified.await;
202+
}
203+
}
204+
205+
/// Reserves `size` bytes unconditionally, without checking capacity.
206+
///
207+
/// The pool may go into overdraft: `used` exceeds `capacity`, `available()`
208+
/// reports 0, and ordinary `try_reserve`/`reserve` callers wait until enough
209+
/// leases are returned to repay the debt. Never waits.
210+
///
211+
/// # Panics
212+
///
213+
/// Panics if the total amount of reserved memory would exceed `usize::MAX`.
214+
#[inline]
215+
pub fn force_reserve(&self, size: usize) -> MemoryLease {
216+
if size == 0 {
217+
return MemoryLease {
218+
budget: self.clone(),
219+
size,
220+
};
221+
}
222+
match &self.inner {
223+
Some(inner) => {
224+
inner
225+
.used
226+
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |used| {
227+
used.checked_add(size)
228+
})
229+
.expect("MemoryPool used counter overflowed");
230+
MemoryLease {
231+
budget: self.clone(),
232+
size,
233+
}
234+
}
235+
None => MemoryLease {
236+
budget: self.clone(),
237+
size,
238+
},
239+
}
240+
}
241+
175242
#[inline]
176243
pub fn empty_lease(&self) -> MemoryLease {
177244
MemoryLease {
@@ -480,7 +547,9 @@ const _: () = {
480547

481548
#[cfg(test)]
482549
mod tests {
550+
use std::assert_matches;
483551
use std::num::NonZeroUsize;
552+
use std::time::Duration;
484553

485554
use super::*;
486555

@@ -730,4 +799,83 @@ mod tests {
730799
assert_eq!(count.load(Ordering::Relaxed), 100);
731800
assert_eq!(budget.used(), bytes(0));
732801
}
802+
803+
#[tokio::test(start_paused = true)]
804+
async fn force_reserve() {
805+
let budget = budget(100);
806+
let r1 = budget.try_reserve(90).expect("should succeed");
807+
assert_eq!(r1.size(), bytes(90));
808+
assert_eq!(budget.used(), bytes(90));
809+
assert_eq!(budget.available(), 10);
810+
assert_eq!(budget.overdraft(), 0);
811+
812+
assert_matches!(budget.try_reserve(20), None);
813+
let r2 = budget.force_reserve(20);
814+
assert_eq!(r2.size(), bytes(20));
815+
assert_eq!(budget.used(), bytes(110));
816+
// available() should still report 0 even though we're in overdraft
817+
assert_eq!(budget.available(), 0);
818+
assert_eq!(budget.overdraft(), 10);
819+
820+
// Try to reserve anything while in overdraft will fail
821+
assert_matches!(budget.try_reserve(1), None);
822+
823+
let r3 = budget.force_reserve(50);
824+
assert_eq!(r3.size(), bytes(50));
825+
assert_eq!(budget.used(), bytes(160));
826+
assert_eq!(budget.available(), 0);
827+
assert_eq!(budget.overdraft(), 60);
828+
829+
let mut waiter1 = std::pin::pin!(budget.reserve(10));
830+
let mut waiter2 = std::pin::pin!(budget.wait_until_available());
831+
832+
// Waiters will be blocked
833+
assert!(
834+
tokio::time::timeout(Duration::from_millis(100), waiter1.as_mut())
835+
.await
836+
.is_err()
837+
);
838+
assert!(
839+
tokio::time::timeout(Duration::from_millis(100), waiter2.as_mut())
840+
.await
841+
.is_err()
842+
);
843+
844+
drop(r3);
845+
assert_eq!(budget.used(), bytes(110));
846+
assert_eq!(budget.available(), 0);
847+
assert_eq!(budget.overdraft(), 10);
848+
849+
// Returning capacity while in overdraft won't fullfill waiters
850+
assert!(
851+
tokio::time::timeout(Duration::from_millis(100), waiter1.as_mut())
852+
.await
853+
.is_err()
854+
);
855+
assert!(
856+
tokio::time::timeout(Duration::from_millis(100), waiter2.as_mut())
857+
.await
858+
.is_err()
859+
);
860+
861+
drop(r2);
862+
assert_eq!(budget.used(), bytes(90));
863+
assert_eq!(budget.available(), 10);
864+
assert_eq!(budget.overdraft(), 0);
865+
866+
// Only then will waiters be unblocked
867+
// waiter2 is waiting for available() to be > 0, so it should be
868+
// immediately unblocked.
869+
assert!(
870+
tokio::time::timeout(Duration::from_millis(100), waiter2.as_mut())
871+
.await
872+
.is_ok()
873+
);
874+
// waiter1 is trying to reserve 10 bytes, which it'll be able to acquire
875+
assert!(
876+
tokio::time::timeout(Duration::from_millis(100), waiter1.as_mut())
877+
.await
878+
.is_ok()
879+
);
880+
}
733881
}

0 commit comments

Comments
 (0)