Skip to main content

moq_net/model/
cache.rs

1//! A shared byte budget for cached groups, repaid by write-time eviction.
2//!
3//! Every group charges its cached bytes into a [`Pool`] through a crate-internal
4//! `Charge`, billed to its track's `Track` account. The pool itself never evicts: it is
5//! a handful of atomic counters. While the pool is over capacity, each track accrues
6//! eviction debt as it writes (`accrue`),
7//! sized proportionally to what it wrote, and pays that debt by aborting its own oldest
8//! groups with [`Error::Evicted`](crate::Error::Evicted). Reclamation is therefore
9//! distributed across every writing track and converges on the capacity without any
10//! global lock, registry, or background task.
11//!
12//! Cross-track ordering comes from one statistic: the mean last-access time of the
13//! evictable population (every cached group except each track's protected latest).
14//! A group accessed more recently than that mean is never evicted, so freshly read
15//! or fetched content in one track can't die while another track holds staler
16//! content, and a track
17//! whose oldest group is staler than the mean accrues debt at double rate. Evicting
18//! old entries and inserting new ones both advance the mean, so the eviction
19//! frontier moves with cache turnover on its own.
20//!
21//! A pool is inert by default ([`Pool::unbounded`]): publishers and subscribers that
22//! never set a capacity pay only a couple of atomic counters. A relay creates one
23//! bounded pool and shares it across every origin so the whole process caches into a
24//! single budget.
25
26use std::sync::Arc;
27use std::sync::atomic::{AtomicU64, Ordering};
28use std::time::Duration;
29
30use super::group;
31use super::track::{self, TrackState};
32
33/// Fixed bookkeeping charged per cached group on top of its frame payload bytes.
34///
35/// A group that holds one small frame is almost entirely bookkeeping: the kio channel
36/// carrying its state, the containers the track indexes it by, and the frame slots
37/// themselves dwarf a chat-sized payload. Billing payload alone lets such a track cache
38/// millions of groups while the pool believes it is inside budget, so the process is
39/// killed before anything is evicted.
40///
41/// Derived from `size_of` rather than pasted from a measured process, so it follows the
42/// structs instead of rotting: each half lives beside the types it sizes, in
43/// [`group::CACHE_OVERHEAD`] and [`track::CACHE_OVERHEAD`]. It excludes what the
44/// allocator rounds up and what a group with many frames grows into, both of which only
45/// matter for shapes payload already dominates.
46///
47/// Also bounds the live group count (`used / ENTRY_OVERHEAD`), which keeps the
48/// access-time sum below u64 (see [`TICK_MS`]).
49pub(crate) const ENTRY_OVERHEAD: u64 = group::CACHE_OVERHEAD + track::CACHE_OVERHEAD;
50
51/// Sub-tick boosts applied to the last-access stamp, breaking ties within one
52/// coarse tick: a frame write outranks merely-inserted content, and a read (a
53/// delivered or fetched group, a frame read, a backfill's birth) outranks both.
54const WRITE_BOOST: u64 = 1;
55const READ_BOOST: u64 = 2;
56
57/// Milliseconds per tick of the coarse clock behind access timestamps.
58///
59/// Coarse ticks keep the count-weighted timestamp sum far from u64 overflow: the
60/// sum is bounded by `elapsed_ticks * live_groups`, live groups are bounded by
61/// `used / ENTRY_OVERHEAD`, and twenty years of ticks (6.3e9) times a 64 GiB
62/// target's worst case of ~70M groups is ~4.5e17, a fortieth of `u64::MAX`. A
63/// byte-weighted mean would overflow u64 even at whole-second ticks, which is why
64/// the mean is count-weighted.
65const TICK_MS: u64 = 100;
66
67/// A shared byte budget that caches charge into; cloning shares the same budget.
68///
69/// The pool tracks how many payload bytes are cached across every registered group,
70/// plus the mean last-access time of the evictable ones. It never evicts on its own:
71/// tracks accrue eviction debt as they write and evict their own oldest groups to
72/// pay it, so every operation here is a few atomics with no lock. The capacity is
73/// therefore a target usage converges toward, not a hard limit: carried debt, capped
74/// payments, and the always-protected live edge all let usage transiently exceed it.
75#[derive(Clone, Default)]
76pub struct Pool {
77	inner: Arc<Inner>,
78}
79
80struct Inner {
81	// Total bytes currently charged, including per-entry overhead.
82	used: AtomicU64,
83	// u64::MAX means unbounded.
84	capacity: AtomicU64,
85	// Reference point for the coarse tick clock.
86	epoch: web_async::time::Instant,
87	// Sum and count of last-access ticks across the evictable population, giving a
88	// count-weighted mean. Tracks add a group when it becomes evictable (demoted
89	// from the live edge, or inserted behind it) and remove it when it leaves.
90	access_sum: AtomicU64,
91	access_count: AtomicU64,
92}
93
94impl Default for Inner {
95	fn default() -> Self {
96		Self {
97			used: AtomicU64::new(0),
98			capacity: AtomicU64::new(u64::MAX),
99			epoch: web_async::time::Instant::now(),
100			access_sum: AtomicU64::new(0),
101			access_count: AtomicU64::new(0),
102		}
103	}
104}
105
106impl Pool {
107	/// Create a pool with a byte target that tracks evict toward as they write.
108	///
109	/// The budget counts frame payload bytes plus a fixed cost per cached group, which
110	/// is most of what a group carrying one small frame occupies. It is not process
111	/// RSS, and it is a convergence target rather than a hard limit; leave headroom
112	/// when sizing it from real memory.
113	pub fn new(capacity: u64) -> Self {
114		let pool = Self::default();
115		pool.inner.capacity.store(capacity, Ordering::Relaxed);
116		pool
117	}
118
119	/// Create a pool that never evicts. This is the [`Default`].
120	pub fn unbounded() -> Self {
121		Self::default()
122	}
123
124	/// The configured byte target, or `None` when unbounded.
125	pub fn capacity(&self) -> Option<u64> {
126		match self.inner.capacity.load(Ordering::Relaxed) {
127			u64::MAX => None,
128			capacity => Some(capacity),
129		}
130	}
131
132	/// Bytes currently cached across every registered group.
133	pub fn used(&self) -> u64 {
134		self.inner.used.load(Ordering::Relaxed)
135	}
136
137	/// Change the capacity. `None` makes the pool unbounded.
138	///
139	/// Takes effect as tracks write: a shrink leaves the pool over budget, which every
140	/// subsequent write pays down proportionally. Nothing is reclaimed synchronously.
141	pub fn resize(&self, capacity: impl Into<Option<u64>>) {
142		let capacity = capacity.into().unwrap_or(u64::MAX);
143		self.inner.capacity.store(capacity, Ordering::Relaxed);
144	}
145
146	/// Returns true if both handles share the same underlying pool.
147	pub fn same_pool(&self, other: &Self) -> bool {
148		Arc::ptr_eq(&self.inner, &other.inner)
149	}
150
151	/// A handle that reaches this budget without keeping it alive.
152	pub fn downgrade(&self) -> PoolWeak {
153		PoolWeak {
154			inner: Arc::downgrade(&self.inner),
155		}
156	}
157
158	/// Charge `n` more cached bytes.
159	pub(crate) fn add(&self, n: u64) {
160		self.inner.used.fetch_add(n, Ordering::Relaxed);
161	}
162
163	/// Release `n` cached bytes.
164	pub(crate) fn sub(&self, n: u64) {
165		self.inner.used.fetch_sub(n, Ordering::Relaxed);
166	}
167
168	/// Coarse ticks since the pool was created: the clock access timestamps use.
169	pub(crate) fn now(&self) -> u64 {
170		self.inner.epoch.elapsed().as_millis() as u64 / TICK_MS
171	}
172
173	/// Convert a duration into coarse ticks, saturating.
174	pub(crate) fn ticks(duration: Duration) -> u64 {
175		u64::try_from(duration.as_millis() / TICK_MS as u128).unwrap_or(u64::MAX)
176	}
177
178	/// Mean last-access tick across the evictable population, or `None` when it is
179	/// empty. The sum and count are read separately, so the mean is approximate
180	/// under concurrent updates; eviction only needs a rough frontier.
181	pub(crate) fn average(&self) -> Option<u64> {
182		let count = self.inner.access_count.load(Ordering::Relaxed);
183		if count == 0 {
184			return None;
185		}
186		Some(self.inner.access_sum.load(Ordering::Relaxed) / count)
187	}
188
189	/// A group with last-access tick `ts` joined the evictable population.
190	pub(crate) fn access_insert(&self, ts: u64) {
191		self.inner.access_sum.fetch_add(ts, Ordering::Relaxed);
192		self.inner.access_count.fetch_add(1, Ordering::Relaxed);
193	}
194
195	/// A group with last-access tick `ts` left the evictable population.
196	pub(crate) fn access_remove(&self, ts: u64) {
197		self.inner.access_sum.fetch_sub(ts, Ordering::Relaxed);
198		self.inner.access_count.fetch_sub(1, Ordering::Relaxed);
199	}
200
201	/// An evictable group's last-access tick moved from `old` to `new` (a FETCH hit).
202	pub(crate) fn access_refresh(&self, old: u64, new: u64) {
203		// A single wrapping add keeps the sum exact even under racing refreshes.
204		self.inner
205			.access_sum
206			.fetch_add(new.wrapping_sub(old), Ordering::Relaxed);
207	}
208
209	/// The eviction debt a track takes on by writing `written` bytes, or `None` while
210	/// the pool is under capacity (the caller should forget any outstanding debt).
211	///
212	/// The debt is `written * used / capacity`, so paying it evicts slightly more
213	/// than was written and the overshoot decays toward the capacity. Tracks double
214	/// it when their oldest content is staler than [`Self::average`]. Saturates: a
215	/// tiny capacity must not wrap a huge debt into a small one.
216	pub(crate) fn accrue(&self, written: u64) -> Option<u64> {
217		let used = self.inner.used.load(Ordering::Relaxed);
218		let capacity = self.inner.capacity.load(Ordering::Relaxed);
219		if used <= capacity {
220			return None;
221		}
222		let debt = written as u128 * used as u128 / capacity.max(1) as u128;
223		Some(u64::try_from(debt).unwrap_or(u64::MAX))
224	}
225}
226
227impl std::fmt::Debug for Pool {
228	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229		f.debug_struct("Pool")
230			.field("used", &self.used())
231			.field("capacity", &self.capacity())
232			.finish()
233	}
234}
235
236/// A handle to a [`Pool`] that does not keep the budget alive.
237///
238/// [`upgrade`](Self::upgrade) stops returning a [`Pool`] once every strong handle has
239/// dropped, which is how a background resizer learns the budget it manages is gone and
240/// nothing can cache into it any more.
241#[derive(Clone)]
242pub struct PoolWeak {
243	inner: std::sync::Weak<Inner>,
244}
245
246impl PoolWeak {
247	/// Recover a [`Pool`], or `None` once every strong handle has dropped.
248	pub fn upgrade(&self) -> Option<Pool> {
249		self.inner.upgrade().map(|inner| Pool { inner })
250	}
251}
252
253impl std::fmt::Debug for PoolWeak {
254	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255		match self.upgrade() {
256			Some(pool) => pool.fmt(f),
257			None => f.debug_struct("PoolWeak").finish_non_exhaustive(),
258		}
259	}
260}
261
262/// Gross bytes a track writes before a frame write settles its eviction debt itself,
263/// so a track appending frames to open groups (never inserting another group) still
264/// pays. Coarse: the cost is one track-state lock per threshold crossing.
265const WRITE_CHARGE_THRESHOLD: u64 = 256 * 1024;
266
267/// One track's account against the [`Pool`], shared with every group it creates.
268///
269/// Groups charge their bytes here (through a [`Charge`]) rather than straight into the
270/// pool, so the track can drain what its own groups wrote into eviction debt and pay it
271/// off by evicting them. The link back to the track is a [`kio::Weak`] because the track
272/// owns its cached groups and each of those owns this account: anything stronger would
273/// make a track's cache immortal.
274///
275/// The default account is detached: an unbounded pool and no track, so every operation
276/// is a no-op.
277#[derive(Default)]
278pub(crate) struct Track {
279	pool: Pool,
280
281	// Gross bytes charged by this track's groups (payload plus overhead), never
282	// decremented here: the track swaps it out as it accrues debt.
283	written: AtomicU64,
284
285	// The track that pays this account off, holding the groups being charged.
286	state: kio::Weak<TrackState>,
287}
288
289impl Track {
290	/// Open an account against `pool` for the track behind `state`.
291	pub(crate) fn new(pool: Pool, state: kio::Weak<TrackState>) -> Arc<Self> {
292		Arc::new(Self {
293			pool,
294			written: AtomicU64::new(0),
295			state,
296		})
297	}
298
299	/// The pool this track caches into.
300	pub(crate) fn pool(&self) -> &Pool {
301		&self.pool
302	}
303
304	/// Charge a new group's fixed overhead, returning its [`Charge`].
305	pub(crate) fn charge(self: &Arc<Self>) -> Charge {
306		self.pool.add(ENTRY_OVERHEAD);
307		self.written.fetch_add(ENTRY_OVERHEAD, Ordering::Relaxed);
308		let last = self.pool.now();
309		Charge {
310			track: Some(self.clone()),
311			bytes: ENTRY_OVERHEAD,
312			last: AtomicU64::new(last),
313			counted: false,
314		}
315	}
316
317	/// Take everything written since the last call, to be turned into eviction debt.
318	pub(crate) fn take_written(&self) -> u64 {
319		self.written.swap(0, Ordering::Relaxed)
320	}
321
322	/// Settle eviction debt from a frame write, once enough bytes accumulate.
323	///
324	/// Called with no group lock held (locks are ordered track then group). Cheap
325	/// until the threshold crosses: one relaxed load. This is what makes a track
326	/// that only appends frames to open groups, never inserting another group,
327	/// still pay its debt (and age its content out).
328	pub(crate) fn settle(&self) {
329		if self.written.load(Ordering::Relaxed) < WRITE_CHARGE_THRESHOLD {
330			return;
331		}
332		// Counts as a producer while it lives, which is why `track::Producer` gates
333		// its teardown on its own clone count rather than the state's.
334		let Some(state) = self.state.upgrade() else { return };
335		if let Ok(mut state) = state.write() {
336			state.charge_debt();
337		}
338	}
339}
340
341/// The RAII byte accounting for one cached group, owned by the group's state.
342///
343/// `add`/`sub` mirror the group's cached payload bytes into the pool with plain
344/// atomics, and every charged byte (overhead included) is also accumulated into the
345/// track's account, which the track drains into eviction debt on its next write. The
346/// charge also owns the group's sample in the pool's access mean, so the sample lives
347/// exactly as long as the cached bytes do: aborting or dropping the group removes both,
348/// no matter who does it or when. The default charge is detached: it belongs to no
349/// account and every operation is a no-op.
350#[derive(Default)]
351pub(crate) struct Charge {
352	track: Option<Arc<Track>>,
353	// Bytes currently charged, including ENTRY_OVERHEAD, released on drop.
354	bytes: u64,
355	// Tick of the last cache access: creation, every write, and every read (group
356	// delivery, frame reads, FETCH hits, a fetched backfill's birth). Eviction
357	// protection and age expiry key off this. Atomic so the read paths can stamp
358	// it through a shared guard: a kio write guard's release notifies every parked
359	// consumer, which a mere access must not do. Accesses are still serialized by
360	// the owning state's lock, so `counted` can pair with it as a plain bool.
361	last: AtomicU64,
362	// Whether `last` is currently a sample in the pool's access mean, i.e. the
363	// group is in the evictable population.
364	counted: bool,
365}
366
367impl Charge {
368	/// Charge `n` more payload bytes, counting them as written.
369	///
370	/// A write is also an access: it restarts the retention clock and keeps an
371	/// actively-growing group (a straggler or backfill still being filled) from
372	/// being evicted or expired mid-write, even within the same coarse tick as
373	/// content that was merely inserted.
374	pub(crate) fn add(&mut self, n: u64) {
375		if let Some(track) = &self.track {
376			track.pool.add(n);
377			track.written.fetch_add(n, Ordering::Relaxed);
378			self.bytes += n;
379		}
380		self.touch(WRITE_BOOST);
381	}
382
383	/// Release `n` payload bytes (a frame evicted by the group's own cap).
384	pub(crate) fn sub(&mut self, n: u64) {
385		if let Some(track) = &self.track {
386			track.pool.sub(n);
387			self.bytes = self.bytes.saturating_sub(n);
388		}
389	}
390
391	/// The group's full cached footprint: payload bytes plus overhead.
392	pub(crate) fn size(&self) -> u64 {
393		self.bytes
394	}
395
396	/// Tick of the group's last cache access.
397	pub(crate) fn accessed(&self) -> u64 {
398		self.last.load(Ordering::Relaxed)
399	}
400
401	/// Enter the group into the evictable population (demoted from the live edge,
402	/// or inserted behind it), sampling its access time into the pool's mean.
403	/// Idempotent.
404	pub(crate) fn demote(&mut self) {
405		if let Some(track) = &self.track
406			&& !self.counted
407		{
408			track.pool.access_insert(self.accessed());
409			self.counted = true;
410		}
411	}
412
413	/// Record a cache read: a delivered or fetched group, a frame read, or a
414	/// fetched backfill's birth. `&self` so the read paths can stamp through a
415	/// shared guard without waking parked consumers.
416	pub(crate) fn refresh(&self) {
417		self.touch(READ_BOOST);
418	}
419
420	/// Record a write that charges no new bytes (a chunk written into an
421	/// already-charged in-flight frame): restarts the retention clock like any
422	/// other write. `&mut self` deliberately: reaching it through a kio write
423	/// guard marks the guard modified, so its release wakes parked readers.
424	pub(crate) fn record_write(&mut self) {
425		self.touch(WRITE_BOOST);
426	}
427
428	/// Advance the last-access tick to `boost` ticks past the coarse clock.
429	///
430	/// The boost breaks ties within one coarse tick: written content outranks
431	/// merely-inserted content, and explicitly read content outranks both, so a
432	/// same-tick access still reads as strictly newer than the population mean of
433	/// weaker accesses. Idempotent within a tick (monotone, never regressing), so
434	/// repeated accesses can't run ahead of the clock by more than the boost.
435	fn touch(&self, boost: u64) {
436		let Some(track) = &self.track else { return };
437		let target = track.pool.now().saturating_add(boost);
438		// `fetch_max` keeps the stamp monotone, and its prior value makes the
439		// paired mean update exact even for back-to-back accesses.
440		let prev = self.last.fetch_max(target, Ordering::Relaxed);
441		if target <= prev {
442			return;
443		}
444		if self.counted {
445			track.pool.access_refresh(prev, target);
446		}
447	}
448
449	/// Release everything this charge holds: bytes, overhead, and the access
450	/// sample. Idempotent; used when the group aborts and clears its frames.
451	pub(crate) fn clear(&mut self) {
452		if let Some(track) = &self.track {
453			track.pool.sub(self.bytes);
454			self.bytes = 0;
455			if self.counted {
456				track.pool.access_remove(self.accessed());
457				self.counted = false;
458			}
459		}
460	}
461}
462
463impl Drop for Charge {
464	fn drop(&mut self) {
465		self.clear();
466	}
467}
468
469#[cfg(test)]
470mod test {
471	use super::*;
472
473	fn charge(pool: &Pool) -> Charge {
474		// No track behind the account: nothing here settles debt, it just accounts.
475		Track::new(pool.clone(), kio::Weak::new()).charge()
476	}
477
478	#[test]
479	fn unbounded_never_accrues() {
480		let pool = Pool::unbounded();
481		let mut charge = charge(&pool);
482		charge.add(1 << 40);
483		assert_eq!(pool.accrue(1 << 30), None);
484		assert_eq!(pool.used(), (1 << 40) + ENTRY_OVERHEAD);
485		drop(charge);
486		assert_eq!(pool.used(), 0);
487	}
488
489	#[test]
490	fn weak_follows_the_last_strong_handle() {
491		let pool = Pool::new(1000);
492		let clone = pool.clone();
493		let weak = pool.downgrade();
494
495		drop(pool);
496		let upgraded = weak.upgrade().expect("a strong handle remains");
497		assert!(upgraded.same_pool(&clone));
498
499		drop(upgraded);
500		drop(clone);
501		assert!(weak.upgrade().is_none());
502	}
503
504	#[test]
505	fn accrue_none_under_capacity() {
506		let pool = Pool::new(ENTRY_OVERHEAD + 1000);
507		let mut charge = charge(&pool);
508		charge.add(500);
509		assert_eq!(pool.accrue(100), None);
510	}
511
512	#[test]
513	fn accrue_proportional_over_capacity() {
514		let pool = Pool::new(1000);
515		let mut charge = charge(&pool);
516		charge.add(2000 - ENTRY_OVERHEAD); // used = 2000, twice the capacity
517
518		// Debt exceeds what was written by the overshoot ratio, so the pool drains.
519		assert_eq!(pool.accrue(100), Some(200));
520		// Zero written accrues zero: an idle track takes on no debt.
521		assert_eq!(pool.accrue(0), Some(0));
522	}
523
524	#[test]
525	fn average_tracks_evictable_population() {
526		let pool = Pool::new(1000);
527		assert_eq!(pool.average(), None);
528
529		pool.access_insert(10);
530		pool.access_insert(20);
531		assert_eq!(pool.average(), Some(15));
532
533		// A refresh moves one member's contribution, exactly.
534		pool.access_refresh(10, 40);
535		assert_eq!(pool.average(), Some(30));
536
537		pool.access_remove(40);
538		assert_eq!(pool.average(), Some(20));
539		pool.access_remove(20);
540		assert_eq!(pool.average(), None);
541	}
542
543	#[test]
544	fn charge_raii() {
545		let pool = Pool::new(1000);
546		let mut charge = charge(&pool);
547		assert_eq!(pool.used(), ENTRY_OVERHEAD);
548
549		charge.add(100);
550		assert_eq!(pool.used(), ENTRY_OVERHEAD + 100);
551		charge.sub(40);
552		assert_eq!(pool.used(), ENTRY_OVERHEAD + 60);
553
554		charge.clear();
555		assert_eq!(pool.used(), 0);
556		// Idempotent: a second clear (and the eventual drop) releases nothing more.
557		charge.clear();
558		drop(charge);
559		assert_eq!(pool.used(), 0);
560	}
561
562	#[test]
563	fn detached_charge_is_noop() {
564		let mut charge = Charge::default();
565		charge.add(123);
566		charge.sub(23);
567		charge.clear();
568	}
569
570	#[test]
571	fn accrue_saturates() {
572		// A huge overshoot against a tiny capacity must saturate, not wrap.
573		let pool = Pool::new(1);
574		let mut c = charge(&pool);
575		c.add(1 << 40);
576		assert_eq!(pool.accrue(1 << 40), Some(u64::MAX));
577	}
578
579	#[test]
580	fn charge_counts_gross_writes() {
581		let track = Track::new(Pool::new(1000), kio::Weak::new());
582		let mut c = track.charge();
583		c.add(100);
584		c.sub(40); // releases don't refund the gross counter
585		assert_eq!(track.take_written(), ENTRY_OVERHEAD + 100);
586		assert_eq!(track.take_written(), 0, "taking it drains the counter");
587	}
588
589	#[test]
590	fn charge_owns_access_sample() {
591		let pool = Pool::new(1000);
592		let mut c = charge(&pool);
593		assert_eq!(pool.average(), None, "not evictable until demoted");
594
595		c.demote();
596		c.demote(); // idempotent
597		assert!(pool.average().is_some());
598
599		// Clearing (an abort, from anyone) removes the sample with the bytes.
600		c.clear();
601		assert_eq!(pool.average(), None, "aborted groups leave no ghost sample");
602		drop(c);
603		assert_eq!(pool.average(), None);
604	}
605
606	#[test]
607	fn refresh_updates_a_counted_sample() {
608		let pool = Pool::new(1000);
609		let mut c = charge(&pool);
610		c.demote();
611		c.refresh();
612		// The sample in the pool mean moved with the stamp, so releasing the charge
613		// removes exactly what was inserted and leaves no residue.
614		assert_eq!(pool.average(), Some(c.accessed()));
615		c.clear();
616		assert_eq!(pool.average(), None);
617	}
618
619	#[test]
620	fn refresh_protects_within_a_tick() {
621		let pool = Pool::new(1000);
622		let mut c = charge(&pool);
623		c.demote();
624		let average = pool.average().unwrap();
625		// A refresh in the same coarse tick still lifts the group above the mean.
626		c.refresh();
627		assert!(c.accessed() > average);
628		// Repeated same-tick refreshes are idempotent, not runaway.
629		let stamped = c.accessed();
630		c.refresh();
631		assert_eq!(c.accessed(), stamped);
632	}
633
634	#[test]
635	fn resize() {
636		let pool = Pool::unbounded();
637		assert_eq!(pool.capacity(), None);
638
639		let mut charge = charge(&pool);
640		charge.add(1000);
641
642		// Shrinking doesn't reclaim anything synchronously; writers accrue debt instead.
643		pool.resize(100);
644		assert_eq!(pool.capacity(), Some(100));
645		assert!(pool.used() > 100);
646		assert!(pool.accrue(50).unwrap() > 50);
647
648		pool.resize(None);
649		assert_eq!(pool.capacity(), None);
650		assert_eq!(pool.accrue(50), None);
651	}
652}