Skip to main content

reifydb_store_multi/flush/
engine.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
5use std::collections::{HashMap, HashSet};
6use std::sync::{Arc, OnceLock};
7
8#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
9use reifydb_codec::key::encoded::EncodedKey;
10#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
11use reifydb_core::event::metric::{MultiEviction, MultiPersist, MultiSweptEvent};
12#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
13use reifydb_core::lifecycle::progress::Progress;
14#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
15use reifydb_core::{
16	common::CommitVersion,
17	default,
18	interface::store::{EntryKind, storage_key},
19};
20use reifydb_core::{event::EventBus, lifecycle::watermark::EvictionWatermark};
21use reifydb_runtime::{
22	context::clock::Clock,
23	sync::{mutex::Mutex, rwlock::RwLock},
24};
25#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
26use reifydb_store_commit::TierBatch;
27use reifydb_store_commit::store::CommitStore;
28#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
29use reifydb_store_commit::store::EvictedVersion;
30#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
31use reifydb_value::byte_size::ByteSize;
32#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
33use reifydb_value::{reifydb_assertions, util::cowvec::CowVec};
34#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
35use tracing::{debug, error, instrument, warn};
36
37#[cfg(all(test, feature = "sqlite", not(target_arch = "wasm32")))]
38use crate::tier::TierStorage;
39use crate::{
40	flush::ObjectPersistence,
41	tier::{persistent::MultiPersistentTier, point::MultiPointTier, range::MultiRangeTier},
42};
43
44#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
45pub const FLUSH_BYTE_BUDGET: ByteSize = if default::TESTING {
46	default::store::MULTI_FLUSH_BUDGET_TESTING
47} else {
48	default::store::MULTI_FLUSH_BUDGET
49};
50
51#[derive(Default)]
52pub struct FlushEngineState {
53	#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
54	resume_from: Option<EntryKind>,
55}
56
57#[allow(dead_code)]
58pub struct FlushEngine {
59	commit: CommitStore,
60	persistent: MultiPersistentTier,
61	persistence: Arc<OnceLock<Arc<dyn ObjectPersistence>>>,
62	eviction_watermark: Arc<RwLock<Option<Arc<dyn EvictionWatermark>>>>,
63	point: Option<MultiPointTier>,
64	range: Option<MultiRangeTier>,
65	clock: Clock,
66	event_bus: EventBus,
67	sweep_lock: Mutex<FlushEngineState>,
68}
69
70#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
71type EvictablePersist = Vec<(EncodedKey, CommitVersion, Option<CowVec<u8>>)>;
72#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
73type EvictableDrop = Vec<EvictedVersion>;
74#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
75type EvictablePartition = (EvictablePersist, EvictableDrop);
76
77#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
78pub struct SweepOutcome {
79	pub progress: Progress,
80	pub reclaimed: u64,
81	pub backlog: u64,
82}
83
84#[cfg(all(feature = "sqlite", not(target_arch = "wasm32")))]
85impl FlushEngine {
86	#[allow(clippy::too_many_arguments)]
87	pub fn new(
88		commit: CommitStore,
89		persistent: MultiPersistentTier,
90		persistence: Arc<OnceLock<Arc<dyn ObjectPersistence>>>,
91		eviction_watermark: Arc<RwLock<Option<Arc<dyn EvictionWatermark>>>>,
92		clock: Clock,
93		event_bus: EventBus,
94	) -> Self {
95		Self {
96			commit,
97			persistent,
98			persistence,
99			eviction_watermark,
100			point: None,
101			range: None,
102			clock,
103			event_bus,
104			sweep_lock: Mutex::new(FlushEngineState::default()),
105		}
106	}
107
108	pub fn with_point(mut self, point: Option<MultiPointTier>) -> Self {
109		self.point = point;
110		self
111	}
112
113	pub fn with_range(mut self, range: Option<MultiRangeTier>) -> Self {
114		self.range = range;
115		self
116	}
117
118	pub fn sweep_slice(&self, budget: ByteSize) -> SweepOutcome {
119		let mut state = self.sweep_lock.lock();
120		let (progress, reclaimed) = match self.eviction_cutoff() {
121			Some(cutoff) => self.sweep_once(&mut state, cutoff, budget),
122			None => (Progress::Exhausted, 0),
123		};
124		SweepOutcome {
125			progress,
126			reclaimed,
127			backlog: self.buffered_entries(),
128		}
129	}
130
131	fn buffered_entries(&self) -> u64 {
132		self.commit
133			.list_all_entry_kinds()
134			.map(|kinds| {
135				kinds.iter().map(|kind| self.commit.estimated_current_count(*kind).unwrap_or(0)).sum()
136			})
137			.unwrap_or(0)
138	}
139
140	pub fn flush_pending(&self) {
141		let mut guard = self.sweep_lock.lock();
142		if let Some(cutoff) = self.eviction_cutoff() {
143			while self.sweep_once(&mut guard, cutoff, FLUSH_BYTE_BUDGET).0.is_yielded() {}
144		}
145	}
146
147	pub fn flush_all(&self) {
148		let mut guard = self.sweep_lock.lock();
149		while self.sweep_once(&mut guard, CommitVersion(u64::MAX), FLUSH_BYTE_BUDGET).0.is_yielded() {}
150	}
151
152	fn eviction_cutoff(&self) -> Option<CommitVersion> {
153		let cutoff = self.eviction_watermark.read().as_ref()?.watermark();
154		if cutoff.0 == 0 {
155			return None;
156		}
157		Some(cutoff)
158	}
159
160	fn is_persistent_object(&self, kind: EntryKind) -> bool {
161		match kind {
162			EntryKind::Source(storage, _) | EntryKind::PartitionedSource(storage, _) => {
163				self.persistence.get().map(|provider| provider.is_persistent(storage)).unwrap_or(true)
164			}
165			EntryKind::Multi => true,
166		}
167	}
168
169	#[cfg(test)]
170	fn sweep(&self, cutoff: CommitVersion) {
171		let mut guard = self.sweep_lock.lock();
172		while self.sweep_once(&mut guard, cutoff, FLUSH_BYTE_BUDGET).0.is_yielded() {}
173	}
174
175	#[instrument(name = "store::multi::flush::sweep_once", level = "debug", skip_all)]
176	fn sweep_once(&self, state: &mut FlushEngineState, cutoff: CommitVersion, budget: ByteSize) -> (Progress, u64) {
177		let Some(mut entry_kinds) = self.list_evictable_kinds() else {
178			return (Progress::Exhausted, 0);
179		};
180		if let Some(resume) = state.resume_from
181			&& let Some(position) = entry_kinds.iter().position(|kind| *kind == resume)
182		{
183			entry_kinds.rotate_left(position);
184		}
185		state.resume_from = None;
186
187		let mut remaining = budget;
188		let mut more = false;
189		let mut plan: Vec<(EntryKind, bool, EvictablePartition)> = Vec::new();
190		let mut batches: HashMap<CommitVersion, TierBatch> = HashMap::new();
191		for kind in entry_kinds {
192			if remaining == ByteSize::ZERO {
193				more = true;
194				state.resume_from = Some(kind);
195				break;
196			}
197			let (to_persist, to_drop, consumed, kind_more) =
198				self.collect_evictable(kind, cutoff, remaining);
199			if to_persist.is_empty() && to_drop.is_empty() {
200				continue;
201			}
202			remaining = remaining.saturating_sub(consumed);
203			more |= kind_more;
204			let persistent_object = self.is_persistent_object(kind);
205			if persistent_object {
206				for (key, version, value) in &to_persist {
207					batches.entry(*version)
208						.or_default()
209						.entry(kind)
210						.or_default()
211						.push((key.clone(), value.clone()));
212				}
213			}
214			plan.push((kind, persistent_object, (to_persist, to_drop)));
215		}
216		if plan.is_empty() {
217			return (Progress::Exhausted, 0);
218		}
219
220		let accepted = if batches.values().any(|batch| !batch.is_empty()) {
221			match self.persistent.persist_sweep(batches.into_iter().collect()) {
222				Ok(accepted) => accepted,
223				Err(e) => {
224					error!(error = %e, "flush sweep: persist failed, aborting slice");
225					return (Progress::Exhausted, 0);
226				}
227			}
228		} else {
229			Vec::new()
230		};
231		let persisted = accepted.len();
232
233		let accepted_keys: HashSet<&[u8]> = accepted.iter().map(|k| k.as_slice()).collect();
234		let mut evictions: Vec<MultiEviction> = Vec::new();
235		let mut persists: Vec<MultiPersist> = Vec::new();
236
237		let mut dropped = 0usize;
238		for (kind, persistent_object, (to_persist, to_drop)) in plan {
239			self.refresh_read_tier(kind, persistent_object, &to_persist, &to_drop, &accepted_keys);
240			if persistent_object {
241				for (key, _, value) in &to_persist {
242					if accepted_keys.contains(key.as_slice()) {
243						persists.push(MultiPersist {
244							key: key.clone(),
245							value_bytes: ByteSize::from_bytes(
246								value.as_ref().map(|v| v.len() as u64).unwrap_or(0),
247							),
248						});
249					}
250				}
251			}
252			for evicted in &to_drop {
253				evictions.push(MultiEviction {
254					key: evicted.key.clone(),
255					value_bytes: evicted.value_bytes,
256					current: evicted.current,
257				});
258			}
259			if let Some(count) = self.drop_from_commit(kind, to_drop) {
260				dropped += count;
261			}
262		}
263
264		if !evictions.is_empty() || !persists.is_empty() {
265			self.event_bus.emit(MultiSweptEvent::new(evictions, persists, cutoff));
266		}
267
268		if persisted > 0 || dropped > 0 {
269			debug!(cutoff = cutoff.0, persisted, dropped, more, "flush sweep slice completed");
270		}
271
272		let progress = if more {
273			Progress::Yielded
274		} else {
275			Progress::Exhausted
276		};
277		(progress, dropped as u64)
278	}
279
280	#[inline]
281	fn list_evictable_kinds(&self) -> Option<Vec<EntryKind>> {
282		match self.commit.list_entry_kinds_by_oldest_pending() {
283			Ok(v) => Some(v),
284			Err(e) => {
285				warn!(error = %e, "flush sweep: list_entry_kinds_by_oldest_pending failed");
286				None
287			}
288		}
289	}
290
291	#[inline]
292	fn collect_evictable(
293		&self,
294		kind: EntryKind,
295		cutoff: CommitVersion,
296		budget: ByteSize,
297	) -> (EvictablePersist, EvictableDrop, ByteSize, bool) {
298		self.commit.collect_evictable_below(kind, cutoff, budget)
299	}
300
301	#[inline]
302	#[instrument(name = "store::multi::flush::refresh_read_tier", level = "debug", skip_all, fields(persist_count = to_persist.len(), drop_count = to_drop.len()))]
303	fn refresh_read_tier(
304		&self,
305		table: EntryKind,
306		persistent_object: bool,
307		to_persist: &[(EncodedKey, CommitVersion, Option<CowVec<u8>>)],
308		to_drop: &[EvictedVersion],
309		accepted: &HashSet<&[u8]>,
310	) {
311		if self.point.is_none() && self.range.is_none() {
312			return;
313		}
314		if persistent_object {
315			for (key, version, value) in to_persist {
316				if accepted.contains(key.as_slice()) {
317					if let Some(range) = &self.range {
318						range.insert(table, key.clone(), *version, value.clone());
319					}
320					if let Some(point) = &self.point {
321						point.insert(
322							table,
323							storage_key(key).1,
324							key.clone(),
325							*version,
326							value.clone(),
327						);
328					}
329				} else {
330					if let Some(range) = &self.range {
331						range.invalidate(table, key);
332					}
333					if let Some(point) = &self.point {
334						point.invalidate(table, storage_key(key).1, key);
335					}
336				}
337			}
338		} else {
339			for evicted in to_drop {
340				if let Some(range) = &self.range {
341					range.invalidate(table, &evicted.key);
342				}
343				if let Some(point) = &self.point {
344					point.invalidate(table, storage_key(&evicted.key).1, &evicted.key);
345				}
346			}
347		}
348	}
349
350	#[inline]
351	fn drop_from_commit(&self, kind: EntryKind, to_drop: EvictableDrop) -> Option<usize> {
352		let drop_count = to_drop.len();
353		reifydb_assertions! {
354			assert!(
355				drop_count > 0,
356				"sweep must only reach drop_from_commit with a non-empty drop set; an empty drop \
357				 issues a no-op commit-buffer drop and lets the dropped counter run for zero work \
358				 (kind={kind:?})"
359			);
360		}
361		let mut batches: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
362		batches.insert(kind, to_drop.into_iter().map(|e| (e.key, e.version)).collect());
363		if let Err(e) = self.commit.compact(batches) {
364			warn!(?kind, error = %e, "flush sweep: commit buffer drop failed");
365			return None;
366		}
367		Some(drop_count)
368	}
369}
370
371#[cfg(all(test, feature = "sqlite", not(target_arch = "wasm32")))]
372mod tests {
373	use std::{
374		collections::hash_map::DefaultHasher,
375		hash::{Hash, Hasher},
376	};
377
378	use reifydb_core::{
379		event::EventListener,
380		interface::{
381			catalog::{id::TableId, storage::StorageId},
382			store::EntryLayout,
383		},
384		key::row::RowKey,
385	};
386	use reifydb_runtime::{actor::system::ActorSystem, shutdown::Shutdown};
387	use reifydb_sqlite::SqliteTempPathGuard;
388	use reifydb_store_commit::VersionedGetResult;
389	use reifydb_value::{util::cowvec::CowVec, value::row_number::RowNumber};
390
391	use super::*;
392	use crate::tier::point::MultiPointConfig;
393
394	fn ek(s: &str) -> EncodedKey {
395		let mut hasher = DefaultHasher::new();
396		s.hash(&mut hasher);
397		RowKey::encoded(StorageId::table(TableId(1)), RowNumber(hasher.finish()))
398	}
399
400	fn val(s: &str) -> CowVec<u8> {
401		CowVec::new(s.as_bytes().to_vec())
402	}
403
404	fn write(buffer: &CommitStore, kind: EntryKind, key: &EncodedKey, version: u64, value: &str) {
405		buffer.set(CommitVersion(version), HashMap::from([(kind, vec![(key.clone(), Some(val(value)))])]))
406			.unwrap();
407	}
408
409	fn budget_for(keys: &[String], value: &str) -> ByteSize {
410		let storage = CommitStore::new();
411		for key in keys {
412			storage.set(
413				CommitVersion(1),
414				HashMap::from([(EntryKind::Multi, vec![(ek(key), Some(val(value)))])]),
415			)
416			.unwrap();
417		}
418		let (_, _, consumed, _) = storage.collect_evictable_below(
419			EntryKind::Multi,
420			CommitVersion(1),
421			ByteSize::from_bytes(u64::MAX),
422		);
423		consumed
424	}
425
426	struct StaticWatermark(CommitVersion);
427
428	impl EvictionWatermark for StaticWatermark {
429		fn watermark(&self) -> CommitVersion {
430			self.0
431		}
432	}
433
434	struct AllPersistent;
435
436	impl ObjectPersistence for AllPersistent {
437		fn is_persistent(&self, _storage: StorageId) -> bool {
438			true
439		}
440	}
441
442	struct NonePersistent;
443
444	impl ObjectPersistence for NonePersistent {
445		fn is_persistent(&self, _storage: StorageId) -> bool {
446			false
447		}
448	}
449
450	fn build_engine(
451		persistence: Arc<dyn ObjectPersistence>,
452		watermark: Option<CommitVersion>,
453	) -> (FlushEngine, SqliteTempPathGuard) {
454		let buffer = CommitStore::new();
455		let (persistent, guard) = MultiPersistentTier::sqlite_in_memory();
456		let persistence_lock: Arc<OnceLock<Arc<dyn ObjectPersistence>>> = Arc::new(OnceLock::new());
457		let _ = persistence_lock.set(persistence);
458		let watermark_lock: Arc<RwLock<Option<Arc<dyn EvictionWatermark>>>> = Arc::new(RwLock::new(None));
459		if let Some(w) = watermark {
460			*watermark_lock.write() = Some(Arc::new(StaticWatermark(w)));
461		}
462		(
463			FlushEngine::new(
464				buffer,
465				persistent,
466				persistence_lock,
467				watermark_lock,
468				Clock::Real,
469				testing_event_bus(),
470			),
471			guard,
472		)
473	}
474
475	fn testing_event_bus() -> EventBus {
476		EventBus::new(&ActorSystem::testing(Clock::testing()).spawner())
477	}
478
479	#[derive(Clone, Default)]
480	struct SweepCollector {
481		events: Arc<Mutex<Vec<MultiSweptEvent>>>,
482	}
483
484	impl EventListener<MultiSweptEvent> for SweepCollector {
485		fn on(&self, event: &MultiSweptEvent) {
486			self.events.lock().push(event.clone());
487		}
488	}
489
490	fn build_engine_watching_sweeps(
491		persistence: Arc<dyn ObjectPersistence>,
492		watermark: CommitVersion,
493	) -> (FlushEngine, SqliteTempPathGuard, SweepCollector) {
494		let buffer = CommitStore::new();
495		let (persistent, guard) = MultiPersistentTier::sqlite_in_memory();
496		let persistence_lock: Arc<OnceLock<Arc<dyn ObjectPersistence>>> = Arc::new(OnceLock::new());
497		let _ = persistence_lock.set(persistence);
498		let watermark_lock: Arc<RwLock<Option<Arc<dyn EvictionWatermark>>>> = Arc::new(RwLock::new(None));
499		*watermark_lock.write() = Some(Arc::new(StaticWatermark(watermark)));
500
501		let event_bus = testing_event_bus();
502		let collector = SweepCollector::default();
503		event_bus.register::<MultiSweptEvent, _>(collector.clone());
504
505		(
506			FlushEngine::new(buffer, persistent, persistence_lock, watermark_lock, Clock::Real, event_bus),
507			guard,
508			collector,
509		)
510	}
511
512	#[test]
513	fn a_sweep_reports_every_version_it_evicted_from_the_commit_buffer() {
514		let (engine, _guard, collector) =
515			build_engine_watching_sweeps(Arc::new(AllPersistent), CommitVersion(2));
516		let kind = EntryKind::Source(StorageId::table(TableId(1)), EntryLayout::Row);
517		let key = ek("k");
518
519		write(&engine.commit, kind, &key, 1, "v1");
520		write(&engine.commit, kind, &key, 2, "v2");
521
522		engine.sweep(CommitVersion(2));
523		engine.event_bus.wait_for_completion();
524
525		let events = collector.events.lock().clone();
526		assert_eq!(events.len(), 1, "one sweep slice must report exactly once");
527
528		let evictions = events[0].evictions();
529		assert_eq!(evictions.len(), 2, "both versions left the buffer and both must be accounted");
530
531		let current: Vec<&MultiEviction> = evictions.iter().filter(|e| e.current).collect();
532		assert_eq!(current.len(), 1, "exactly one of the two was the live version");
533		assert_eq!(
534			current[0].value_bytes,
535			ByteSize::from_bytes(2),
536			"the evicted bytes must be the value's own, not a placeholder"
537		);
538
539		let superseded: Vec<&MultiEviction> = evictions.iter().filter(|e| !e.current).collect();
540		assert_eq!(superseded.len(), 1, "v1 was superseded by v2 and is discarded, not persisted");
541
542		let persists = events[0].persists();
543		assert_eq!(persists.len(), 1, "only the latest version below the cutoff reaches the persistent tier");
544		assert_eq!(persists[0].value_bytes, ByteSize::from_bytes(2));
545	}
546
547	#[test]
548	fn a_sweep_that_persists_nothing_still_reports_what_it_discarded() {
549		let (engine, _guard, collector) =
550			build_engine_watching_sweeps(Arc::new(NonePersistent), CommitVersion(2));
551		let kind = EntryKind::Source(StorageId::table(TableId(1)), EntryLayout::Row);
552		let key = ek("k");
553
554		write(&engine.commit, kind, &key, 1, "v1");
555		write(&engine.commit, kind, &key, 2, "v2");
556
557		engine.sweep(CommitVersion(2));
558		engine.event_bus.wait_for_completion();
559
560		let events = collector.events.lock().clone();
561		assert_eq!(events.len(), 1);
562		assert_eq!(events[0].evictions().len(), 2, "the discarded versions are still reported");
563		assert!(events[0].persists().is_empty(), "a non-persistent object persists nothing");
564	}
565
566	fn build_engine_with_point(
567		persistence: Arc<dyn ObjectPersistence>,
568		watermark: CommitVersion,
569		point: MultiPointTier,
570	) -> (FlushEngine, SqliteTempPathGuard) {
571		let buffer = CommitStore::new();
572		let (persistent, guard) = MultiPersistentTier::sqlite_in_memory();
573		let persistence_lock: Arc<OnceLock<Arc<dyn ObjectPersistence>>> = Arc::new(OnceLock::new());
574		let _ = persistence_lock.set(persistence);
575		let watermark_lock: Arc<RwLock<Option<Arc<dyn EvictionWatermark>>>> = Arc::new(RwLock::new(None));
576		*watermark_lock.write() = Some(Arc::new(StaticWatermark(watermark)));
577		(
578			FlushEngine::new(
579				buffer,
580				persistent,
581				persistence_lock,
582				watermark_lock,
583				Clock::Real,
584				testing_event_bus(),
585			)
586			.with_point(Some(point)),
587			guard,
588		)
589	}
590
591	#[test]
592	fn eviction_cutoff_is_none_without_watermark() {
593		let (actor, _guard) = build_engine(Arc::new(AllPersistent), None);
594		assert!(actor.eviction_cutoff().is_none(), "no watermark set => no eviction");
595	}
596
597	#[test]
598	fn eviction_cutoff_is_none_at_zero() {
599		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(0)));
600		assert!(actor.eviction_cutoff().is_none());
601	}
602
603	#[test]
604	fn a_pinned_cutoff_reports_the_entries_it_could_not_release() {
605		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1)));
606		let kind = EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row);
607		for i in 0..8u64 {
608			write(&actor.commit, kind, &ek(&format!("k{i}")), 10 + i, "v");
609		}
610
611		let outcome = actor.sweep_slice(FLUSH_BYTE_BUDGET);
612
613		assert_eq!(outcome.reclaimed, 0, "nothing is below the pinned cutoff, so nothing can be reclaimed");
614		assert_eq!(
615			outcome.backlog, 8,
616			"the entries the cutoff could not release must still be reported, or a pinned floor \
617			 looks exactly like an idle one"
618		);
619		assert!(
620			outcome.progress.is_exhausted(),
621			"budget exhaustion must not be the backlog signal: a pinned cutoff collects nothing and \
622			 therefore never reports more work to do"
623		);
624	}
625
626	#[test]
627	fn a_cutoff_that_can_release_reports_what_it_reclaimed() {
628		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(20)));
629		let kind = EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row);
630		for i in 0..8u64 {
631			write(&actor.commit, kind, &ek(&format!("k{i}")), 10 + i, "v");
632		}
633
634		let outcome = actor.sweep_slice(FLUSH_BYTE_BUDGET);
635
636		assert!(outcome.reclaimed > 0, "entries below the cutoff must count as work done");
637		assert_eq!(outcome.backlog, 0, "a drained buffer reports no backlog");
638	}
639
640	#[test]
641	fn sweep_persists_then_evicts_persistent_object_below_watermark() {
642		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(2)));
643		let kind = EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row);
644		let key = ek("k");
645		write(&actor.commit, kind, &key, 1, "v1");
646		write(&actor.commit, kind, &key, 2, "v2");
647		write(&actor.commit, kind, &key, 3, "v3");
648
649		actor.sweep(CommitVersion(2));
650
651		assert!(
652			matches!(
653				actor.commit.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
654				VersionedGetResult::NotFound
655			),
656			"v2 must be gone from the buffer after eviction"
657		);
658		assert!(
659			matches!(
660				actor.persistent.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
661				VersionedGetResult::Value { .. }
662			),
663			"v2 must survive in the persistent tier"
664		);
665
666		assert_eq!(
667			actor.commit.get(kind, key.as_ref(), CommitVersion(3)).unwrap().value().as_deref(),
668			Some(b"v3".as_slice()),
669			"v3 (> cutoff) must stay in the buffer"
670		);
671	}
672
673	#[test]
674	fn sweep_evicts_non_persistent_object_without_persisting() {
675		let (actor, _guard) = build_engine(Arc::new(NonePersistent), Some(CommitVersion(2)));
676		let kind = EntryKind::Source(StorageId::Table(TableId(7)), EntryLayout::Row);
677		let key = ek("ephemeral");
678		write(&actor.commit, kind, &key, 1, "v1");
679		write(&actor.commit, kind, &key, 2, "v2");
680		write(&actor.commit, kind, &key, 3, "v3");
681
682		actor.sweep(CommitVersion(2));
683
684		assert!(
685			matches!(
686				actor.commit.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
687				VersionedGetResult::NotFound
688			),
689			"non-persistent object must still be evicted below the watermark"
690		);
691		assert!(
692			matches!(
693				actor.persistent.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
694				VersionedGetResult::NotFound
695			),
696			"non-persistent object must NOT be written to the persistent tier"
697		);
698		assert_eq!(
699			actor.commit.get(kind, key.as_ref(), CommitVersion(3)).unwrap().value().as_deref(),
700			Some(b"v3".as_slice()),
701			"v3 (> cutoff) must stay resident even for a non-persistent object"
702		);
703	}
704
705	#[test]
706	fn sweep_keeps_everything_when_all_above_watermark() {
707		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1)));
708		let kind = EntryKind::Source(StorageId::Table(TableId(3)), EntryLayout::Row);
709		let key = ek("k");
710		write(&actor.commit, kind, &key, 5, "v5");
711
712		actor.sweep(CommitVersion(1));
713
714		assert_eq!(
715			actor.commit.get(kind, key.as_ref(), CommitVersion(5)).unwrap().value().as_deref(),
716			Some(b"v5".as_slice()),
717			"a version above the watermark must never be evicted"
718		);
719		assert!(
720			matches!(
721				actor.persistent.get(kind, key.as_ref(), CommitVersion(5)).unwrap(),
722				VersionedGetResult::NotFound
723			),
724			"nothing below the watermark => nothing persisted"
725		);
726	}
727
728	#[test]
729	fn sweep_seeds_evicted_keys_into_the_read_tier() {
730		let point = MultiPointTier::new(MultiPointConfig::testing()).unwrap();
731		let (actor, _guard) = build_engine_with_point(Arc::new(AllPersistent), CommitVersion(2), point.clone());
732		let kind = EntryKind::Source(StorageId::Table(TableId(11)), EntryLayout::Row);
733		let key = ek("k");
734		write(&actor.commit, kind, &key, 1, "v1");
735		write(&actor.commit, kind, &key, 2, "v2");
736
737		point.insert(kind, storage_key(&key).1, key.clone(), CommitVersion(2), Some(val("stale")));
738
739		actor.sweep(CommitVersion(2));
740
741		match point.get(kind, storage_key(&key).1, &key, CommitVersion(2)) {
742			VersionedGetResult::Value {
743				value,
744				..
745			} => assert_eq!(
746				value.as_ref(),
747				val("v2").as_ref(),
748				"the point tier must hold the persisted value, not the stale one"
749			),
750			other => panic!("the sweep must seed the evicted key into the point tier, got {other:?}"),
751		}
752	}
753
754	#[test]
755	fn sweep_seeds_a_delete_of_a_persisted_row_into_the_read_tier() {
756		let point = MultiPointTier::new(MultiPointConfig::testing()).unwrap();
757		let (actor, _guard) = build_engine_with_point(Arc::new(AllPersistent), CommitVersion(2), point.clone());
758		let kind = EntryKind::Source(StorageId::Table(TableId(21)), EntryLayout::Row);
759		let key = ek("k");
760		actor.persistent
761			.set(CommitVersion(1), HashMap::from([(kind, vec![(key.clone(), Some(val("v1")))])]))
762			.unwrap();
763		point.insert(kind, storage_key(&key).1, key.clone(), CommitVersion(1), Some(val("v1")));
764		write(&actor.commit, kind, &key, 1, "v1");
765		actor.commit.set(CommitVersion(2), HashMap::from([(kind, vec![(key.clone(), None)])])).unwrap();
766
767		actor.sweep(CommitVersion(2));
768
769		assert!(
770			matches!(
771				point.get(kind, storage_key(&key).1, &key, CommitVersion(2)),
772				VersionedGetResult::Tombstone
773			),
774			"an evicted delete that matched a persisted row must be seeded into the point tier as a \
775			 definitive miss, never left holding the value it deleted"
776		);
777	}
778
779	#[test]
780	fn sweep_invalidates_rejected_key_but_seeds_accepted() {
781		let point = MultiPointTier::new(MultiPointConfig::testing()).unwrap();
782		let (actor, _guard) = build_engine_with_point(Arc::new(AllPersistent), CommitVersion(2), point.clone());
783		let kind = EntryKind::Source(StorageId::Table(TableId(22)), EntryLayout::Row);
784		let rejected = ek("rejected");
785		let accepted = ek("accepted");
786
787		actor.persistent
788			.set(CommitVersion(3), HashMap::from([(kind, vec![(rejected.clone(), Some(val("high")))])]))
789			.unwrap();
790
791		point.insert(kind, storage_key(&rejected).1, rejected.clone(), CommitVersion(2), Some(val("stale")));
792
793		write(&actor.commit, kind, &rejected, 2, "low");
794		write(&actor.commit, kind, &accepted, 2, "b");
795
796		actor.sweep(CommitVersion(2));
797
798		assert!(
799			matches!(
800				point.get(kind, storage_key(&rejected).1, &rejected, CommitVersion(2)),
801				VersionedGetResult::NotFound
802			),
803			"a guard-rejected key must be invalidated in the point tier so reads fall through to the newer \
804			 persisted value, never serving the stale entry"
805		);
806		match point.get(kind, storage_key(&accepted).1, &accepted, CommitVersion(2)) {
807			VersionedGetResult::Value {
808				value,
809				..
810			} => assert_eq!(value.as_ref(), val("b").as_ref(), "the accepted key must be seeded"),
811			other => panic!("the accepted key must be seeded into the point tier, got {other:?}"),
812		}
813	}
814
815	#[test]
816	fn sweep_seed_respects_read_tier_downgrade_guard() {
817		let point = MultiPointTier::new(MultiPointConfig::testing()).unwrap();
818		let (actor, _guard) = build_engine_with_point(Arc::new(AllPersistent), CommitVersion(2), point.clone());
819		let kind = EntryKind::Source(StorageId::Table(TableId(23)), EntryLayout::Row);
820		let key = ek("k");
821
822		point.insert(kind, storage_key(&key).1, key.clone(), CommitVersion(5), Some(val("newer")));
823
824		write(&actor.commit, kind, &key, 2, "older");
825		actor.sweep(CommitVersion(2));
826
827		match point.get(kind, storage_key(&key).1, &key, CommitVersion(5)) {
828			VersionedGetResult::Value {
829				value,
830				..
831			} => assert_eq!(
832				value.as_ref(),
833				val("newer").as_ref(),
834				"the older seeded value must not overwrite a newer resident point-tier entry"
835			),
836			other => panic!("the newer point-tier entry must survive the sweep's seed, got {other:?}"),
837		}
838	}
839
840	#[test]
841	fn sweep_invalidates_ephemeral_object_in_read_tier() {
842		let point = MultiPointTier::new(MultiPointConfig::testing()).unwrap();
843		let (actor, _guard) =
844			build_engine_with_point(Arc::new(NonePersistent), CommitVersion(2), point.clone());
845		let kind = EntryKind::Source(StorageId::Table(TableId(24)), EntryLayout::Row);
846		let key = ek("k");
847
848		point.insert(kind, storage_key(&key).1, key.clone(), CommitVersion(2), Some(val("stale")));
849		write(&actor.commit, kind, &key, 2, "v2");
850
851		actor.sweep(CommitVersion(2));
852
853		assert!(
854			matches!(
855				point.get(kind, storage_key(&key).1, &key, CommitVersion(2)),
856				VersionedGetResult::NotFound
857			),
858			"an ephemeral (persistent:false) object must be invalidated in the point tier, never seeded"
859		);
860		assert!(
861			matches!(
862				actor.persistent.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
863				VersionedGetResult::NotFound
864			),
865			"an ephemeral object must not be persisted"
866		);
867	}
868
869	#[test]
870	fn sweep_seeds_accepted_keys_across_version_buckets() {
871		let point = MultiPointTier::new(MultiPointConfig::testing()).unwrap();
872		let (actor, _guard) = build_engine_with_point(Arc::new(AllPersistent), CommitVersion(4), point.clone());
873		let kind = EntryKind::Source(StorageId::Table(TableId(25)), EntryLayout::Row);
874		let a = ek("a");
875		let b = ek("b");
876		write(&actor.commit, kind, &a, 1, "a1");
877		write(&actor.commit, kind, &a, 2, "a2");
878		write(&actor.commit, kind, &b, 3, "b3");
879		write(&actor.commit, kind, &b, 4, "b4");
880
881		actor.sweep(CommitVersion(4));
882
883		match point.get(kind, storage_key(&a).1, &a, CommitVersion(4)) {
884			VersionedGetResult::Value {
885				value,
886				..
887			} => assert_eq!(value.as_ref(), val("a2").as_ref(), "a's latest-<=W (v2) must be seeded"),
888			other => panic!("key a must be seeded across version buckets, got {other:?}"),
889		}
890		match point.get(kind, storage_key(&b).1, &b, CommitVersion(4)) {
891			VersionedGetResult::Value {
892				value,
893				..
894			} => assert_eq!(value.as_ref(), val("b4").as_ref(), "b's latest-<=W (v4) must be seeded"),
895			other => panic!("key b must be seeded across version buckets, got {other:?}"),
896		}
897	}
898
899	#[test]
900	fn sweep_removes_the_persisted_row_so_deleted_keys_stay_deleted_after_eviction() {
901		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(2)));
902		let kind = EntryKind::Source(StorageId::Table(TableId(12)), EntryLayout::Row);
903		let key = ek("k");
904		actor.persistent
905			.set(CommitVersion(1), HashMap::from([(kind, vec![(key.clone(), Some(val("v1")))])]))
906			.unwrap();
907		write(&actor.commit, kind, &key, 1, "v1");
908		actor.commit.set(CommitVersion(2), HashMap::from([(kind, vec![(key.clone(), None)])])).unwrap();
909
910		actor.sweep(CommitVersion(2));
911
912		assert!(
913			matches!(
914				actor.commit.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
915				VersionedGetResult::NotFound
916			),
917			"both versions are gone from the buffer"
918		);
919		assert!(
920			matches!(
921				actor.persistent.get(kind, key.as_ref(), CommitVersion(2)).unwrap(),
922				VersionedGetResult::NotFound
923			),
924			"the persisted row must be gone - leaving it behind resurrects v1 once the buffer drops it"
925		);
926	}
927
928	#[test]
929	fn sweep_evicts_below_and_keeps_above_across_multiple_keys() {
930		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(2)));
931		let kind = EntryKind::Source(StorageId::Table(TableId(13)), EntryLayout::Row);
932		let cold = ek("cold");
933		let hot = ek("hot");
934		write(&actor.commit, kind, &cold, 1, "cold1");
935		write(&actor.commit, kind, &hot, 4, "hot4");
936
937		actor.sweep(CommitVersion(2));
938
939		assert!(
940			matches!(
941				actor.commit.get(kind, cold.as_ref(), CommitVersion(2)).unwrap(),
942				VersionedGetResult::NotFound
943			),
944			"cold (v1 <= cutoff) must be evicted from the buffer"
945		);
946		assert!(
947			matches!(
948				actor.persistent.get(kind, cold.as_ref(), CommitVersion(2)).unwrap(),
949				VersionedGetResult::Value { .. }
950			),
951			"cold must survive in persistent"
952		);
953		assert_eq!(
954			actor.commit.get(kind, hot.as_ref(), CommitVersion(4)).unwrap().value().as_deref(),
955			Some(b"hot4".as_slice()),
956			"hot (v4 > cutoff) must stay resident in the buffer"
957		);
958		assert!(
959			matches!(
960				actor.persistent.get(kind, hot.as_ref(), CommitVersion(4)).unwrap(),
961				VersionedGetResult::NotFound
962			),
963			"hot must not be persisted - it is above the watermark"
964		);
965	}
966
967	#[test]
968	fn flush_all_persists_every_key_regardless_of_watermark() {
969		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1)));
970		let kind = EntryKind::Source(StorageId::Table(TableId(101)), EntryLayout::Row);
971		let cold = ek("cold");
972		let hot = ek("hot");
973		write(&actor.commit, kind, &cold, 2, "cold2");
974		write(&actor.commit, kind, &hot, 50, "hot50");
975
976		actor.sweep(CommitVersion(u64::MAX));
977
978		assert_eq!(
979			actor.persistent.get(kind, cold.as_ref(), CommitVersion(u64::MAX)).unwrap().value().as_deref(),
980			Some(b"cold2".as_slice()),
981			"a key committed above the watermark must be persisted by a full flush"
982		);
983		assert_eq!(
984			actor.persistent.get(kind, hot.as_ref(), CommitVersion(u64::MAX)).unwrap().value().as_deref(),
985			Some(b"hot50".as_slice()),
986			"the latest committed value of every key must survive a full flush"
987		);
988		assert!(
989			matches!(
990				actor.commit.get(kind, hot.as_ref(), CommitVersion(u64::MAX)).unwrap(),
991				VersionedGetResult::NotFound
992			),
993			"a full flush drains the buffer after persisting"
994		);
995	}
996
997	#[test]
998	fn sweep_aborts_and_keeps_buffer_when_persist_fails() {
999		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(2)));
1000		let row_kind = EntryKind::Source(StorageId::Table(TableId(31)), EntryLayout::Row);
1001		let dict_kind = EntryKind::Multi;
1002		let row_key = ek("row-referencing-id-7");
1003		let dict_key = ek("dictionary-entry-7");
1004		write(&actor.commit, row_kind, &row_key, 1, "id=7");
1005		write(&actor.commit, dict_kind, &dict_key, 1, "entry-7");
1006
1007		actor.persistent.shutdown();
1008		actor.sweep(CommitVersion(2));
1009
1010		assert_eq!(
1011			actor.commit.get(row_kind, row_key.as_ref(), CommitVersion(2)).unwrap().value().as_deref(),
1012			Some(b"id=7".as_slice()),
1013			"a failed persist must leave the row write in the commit buffer, not drop the only copy"
1014		);
1015		assert_eq!(
1016			actor.commit.get(dict_kind, dict_key.as_ref(), CommitVersion(2)).unwrap().value().as_deref(),
1017			Some(b"entry-7".as_slice()),
1018			"a failed persist must leave the dictionary write in the commit buffer, not drop the only copy"
1019		);
1020	}
1021
1022	#[test]
1023	fn persist_sweep_errors_when_storage_is_shut_down() {
1024		let (persistent, _guard) = MultiPersistentTier::sqlite_in_memory();
1025		persistent.shutdown();
1026
1027		let kind = EntryKind::Source(StorageId::Table(TableId(32)), EntryLayout::Row);
1028		let batches = vec![(CommitVersion(1), HashMap::from([(kind, vec![(ek("k"), Some(val("v")))])]))];
1029		assert!(
1030			persistent.persist_sweep(batches).is_err(),
1031			"a shut-down persistent tier must refuse the sweep loudly so the buffer is not dropped"
1032		);
1033	}
1034
1035	#[test]
1036	fn sweep_persists_all_kinds_and_versions_together() {
1037		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(3)));
1038		let row_kind = EntryKind::Source(StorageId::Table(TableId(33)), EntryLayout::Row);
1039		let dict_kind = EntryKind::Multi;
1040		let row_key = ek("row-referencing-id-9");
1041		let dict_key = ek("dictionary-entry-9");
1042		write(&actor.commit, row_kind, &row_key, 3, "id=9");
1043		write(&actor.commit, dict_kind, &dict_key, 2, "entry-9");
1044
1045		actor.sweep(CommitVersion(3));
1046
1047		assert_eq!(
1048			actor.persistent.get(row_kind, row_key.as_ref(), CommitVersion(3)).unwrap().value().as_deref(),
1049			Some(b"id=9".as_slice()),
1050			"the row write must be durable after the sweep"
1051		);
1052		assert_eq!(
1053			actor.persistent
1054				.get(dict_kind, dict_key.as_ref(), CommitVersion(3))
1055				.unwrap()
1056				.value()
1057				.as_deref(),
1058			Some(b"entry-9".as_slice()),
1059			"the dictionary write committed at an earlier version must be durable in the same sweep"
1060		);
1061		assert!(
1062			matches!(
1063				actor.commit.get(row_kind, row_key.as_ref(), CommitVersion(3)).unwrap(),
1064				VersionedGetResult::NotFound
1065			),
1066			"a persisted row write must be drained from the buffer"
1067		);
1068		assert!(
1069			matches!(
1070				actor.commit.get(dict_kind, dict_key.as_ref(), CommitVersion(3)).unwrap(),
1071				VersionedGetResult::NotFound
1072			),
1073			"a persisted dictionary write must be drained from the buffer"
1074		);
1075	}
1076
1077	#[test]
1078	fn flush_all_removes_the_persisted_row_for_a_delete_above_the_watermark() {
1079		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1)));
1080		let kind = EntryKind::Source(StorageId::Table(TableId(102)), EntryLayout::Row);
1081		let key = ek("k");
1082		actor.persistent
1083			.set(CommitVersion(5), HashMap::from([(kind, vec![(key.clone(), Some(val("v5")))])]))
1084			.unwrap();
1085		write(&actor.commit, kind, &key, 5, "v5");
1086		actor.commit.set(CommitVersion(9), HashMap::from([(kind, vec![(key.clone(), None)])])).unwrap();
1087
1088		actor.sweep(CommitVersion(u64::MAX));
1089
1090		assert!(
1091			matches!(
1092				actor.persistent.get(kind, key.as_ref(), CommitVersion(u64::MAX)).unwrap(),
1093				VersionedGetResult::NotFound
1094			),
1095			"a delete committed above the watermark must remove the persisted row, not resurrect v5"
1096		);
1097	}
1098
1099	#[test]
1100	fn sweep_persists_multi_kind_entries() {
1101		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(10)));
1102		let kind = EntryKind::Multi;
1103		let key = ek("dictionary-entry");
1104		write(&actor.commit, kind, &key, 5, "mint-id-7");
1105
1106		actor.sweep(CommitVersion(10));
1107
1108		assert!(
1109			matches!(
1110				actor.persistent.get(kind, key.as_ref(), CommitVersion(10)).unwrap(),
1111				VersionedGetResult::Value { .. }
1112			),
1113			"a Multi entry committed below the watermark must reach the persistent tier; \
1114			 dictionary entries and CDC checkpoints live in this keyspace and are lost on restart if it does not"
1115		);
1116	}
1117
1118	#[test]
1119	fn a_kind_behind_the_budget_prefix_is_still_swept_under_sustained_writes() {
1120		const KINDS: u64 = 40;
1121		const KEYS_PER_ROUND: u64 = 20;
1122		const KEYS_PER_SLICE: u64 = 40;
1123		const ROUNDS: u64 = 60;
1124		const FIRST_VERSION: u64 = 1;
1125
1126		let budget = budget_for(&(0..KEYS_PER_SLICE).map(|i| format!("v1-k{i}")).collect::<Vec<_>>(), "x");
1127
1128		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1_000_000)));
1129		let kinds: Vec<EntryKind> = (0..KINDS)
1130			.map(|i| EntryKind::Source(StorageId::Table(TableId(i + 1)), EntryLayout::Row))
1131			.collect();
1132
1133		let chronological_key = |version: u64, key: u64| {
1134			let sequence = version * KEYS_PER_ROUND + key;
1135			RowKey::encoded(StorageId::table(TableId(1)), RowNumber(u64::MAX - sequence))
1136		};
1137
1138		let round_writes = |version: u64| {
1139			for kind in &kinds {
1140				for key in 0..KEYS_PER_ROUND {
1141					write(&actor.commit, *kind, &chronological_key(version, key), version, "x");
1142				}
1143			}
1144		};
1145
1146		round_writes(FIRST_VERSION);
1147		let mut exhausted = 0;
1148		for round in 0..ROUNDS {
1149			if actor.sweep_slice(budget).progress.is_yielded() {
1150				exhausted += 1;
1151			}
1152			round_writes(FIRST_VERSION + 1 + round);
1153		}
1154
1155		assert_eq!(
1156			exhausted, ROUNDS,
1157			"the budget must run out every slice for this to exercise starvation at all"
1158		);
1159
1160		let oldest = actor.commit.oldest_pending_version().expect("writes are still pending");
1161		assert!(
1162			oldest.0 > FIRST_VERSION,
1163			"the oldest pending version is still {} after {ROUNDS} slices, so at least one of the \
1164			 {KINDS} kinds was never swept once; that kind pins the durable frontier at the first \
1165			 write, which clamps the tombstone reap cutoff to zero and leaves every tombstone in the \
1166			 persistent tier undeletable",
1167			oldest.0
1168		);
1169	}
1170
1171	#[test]
1172	fn a_kind_that_sorts_behind_a_deeper_backlog_is_still_reached_by_the_sweep() {
1173		const HOT_KINDS: u64 = 3;
1174		const KEYS_PER_ROUND: u64 = 40;
1175		const KEYS_PER_SLICE: u64 = 30;
1176		const ROUNDS: u64 = 80;
1177		const COLD_FIRST_VERSION: u64 = 50;
1178
1179		let budget = budget_for(&(0..KEYS_PER_SLICE).map(|i| format!("v1-k{i}")).collect::<Vec<_>>(), "x");
1180
1181		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1_000_000)));
1182		let hot: Vec<EntryKind> = (0..HOT_KINDS)
1183			.map(|i| EntryKind::Source(StorageId::Table(TableId(i + 1)), EntryLayout::Row))
1184			.collect();
1185		let cold = EntryKind::Source(StorageId::Table(TableId(HOT_KINDS + 1)), EntryLayout::Row);
1186
1187		for round in 1..=ROUNDS {
1188			for kind in &hot {
1189				for key in 0..KEYS_PER_ROUND {
1190					write(&actor.commit, *kind, &ek(&format!("v{round}-k{key}")), round, "x");
1191				}
1192			}
1193			if round == COLD_FIRST_VERSION {
1194				write(&actor.commit, cold, &ek("cold-key"), round, "x");
1195			}
1196			actor.sweep_slice(budget);
1197		}
1198
1199		assert!(
1200			actor.commit.oldest_pending_for(hot[0]).is_some(),
1201			"the hot kinds must stay backlogged, otherwise the budget never ran out and this exercises nothing"
1202		);
1203		assert_eq!(
1204			actor.commit.oldest_pending_for(cold),
1205			None,
1206			"the single write to the cold kind is still pending after {} slices, so the sweep never reached past the hot kinds sorted ahead of it",
1207			ROUNDS - COLD_FIRST_VERSION
1208		);
1209	}
1210
1211	#[test]
1212	fn a_slice_cut_off_mid_kind_resumes_past_it_instead_of_restarting_at_the_head() {
1213		const DEEP_KEYS: u64 = 400;
1214		const KEYS_PER_SLICE: u64 = 10;
1215		const SLICES: u64 = 20;
1216		const COLD_VERSION: u64 = 50;
1217
1218		let budget = budget_for(&(0..KEYS_PER_SLICE).map(|i| format!("deep-k{i}")).collect::<Vec<_>>(), "x");
1219
1220		let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1_000_000)));
1221		let deep = EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row);
1222		let cold = EntryKind::Source(StorageId::Table(TableId(2)), EntryLayout::Row);
1223
1224		for key in 0..DEEP_KEYS {
1225			write(&actor.commit, deep, &ek(&format!("deep-k{key}")), 1, "x");
1226		}
1227		write(&actor.commit, cold, &ek("cold-k"), COLD_VERSION, "x");
1228
1229		for _ in 0..SLICES {
1230			actor.sweep_slice(budget);
1231		}
1232
1233		assert_eq!(
1234			actor.commit.oldest_pending_for(deep),
1235			Some(CommitVersion(1)),
1236			"the deep kind must still hold the oldest pending version after every slice, otherwise it \
1237			 stopped ranking ahead of the cold kind and this never exercised the cursor at all"
1238		);
1239		assert_eq!(
1240			actor.commit.oldest_pending_for(cold),
1241			None,
1242			"the cold write is still pending after {SLICES} slices, so every slice restarted at the deep \
1243			 kind the ranking puts first and never resumed past the kind it was cut off in"
1244		);
1245	}
1246
1247	#[test]
1248	fn a_slice_with_no_resume_point_serves_the_kind_holding_the_oldest_pending_write() {
1249		const KINDS: u64 = 6;
1250		const OLD_VERSION: u64 = 1;
1251		const YOUNG_VERSION: u64 = 100;
1252
1253		let budget = budget_for(&["k".to_string()], "v");
1254
1255		for oldest in 0..KINDS {
1256			let (actor, _guard) = build_engine(Arc::new(AllPersistent), Some(CommitVersion(1_000_000)));
1257			let kinds: Vec<EntryKind> = (0..KINDS)
1258				.map(|i| EntryKind::Source(StorageId::Table(TableId(i + 1)), EntryLayout::Row))
1259				.collect();
1260			for (index, kind) in kinds.iter().enumerate() {
1261				let version = if index as u64 == oldest {
1262					OLD_VERSION
1263				} else {
1264					YOUNG_VERSION
1265				};
1266				write(&actor.commit, *kind, &ek("k"), version, "v");
1267			}
1268
1269			actor.sweep_slice(budget);
1270
1271			assert_eq!(
1272				actor.commit.oldest_pending_for(kinds[oldest as usize]),
1273				None,
1274				"a budget covering exactly one entry must buy the kind holding the oldest pending \
1275				 write, but the kind at version {OLD_VERSION} was left pending while a younger kind \
1276				 took the slice; the durable frontier is the minimum over kinds, so serving anything \
1277				 but the oldest cannot advance it"
1278			);
1279			for (index, kind) in kinds.iter().enumerate() {
1280				if index as u64 == oldest {
1281					continue;
1282				}
1283				assert_eq!(
1284					actor.commit.oldest_pending_for(*kind),
1285					Some(CommitVersion(YOUNG_VERSION)),
1286					"the budget covered one entry, so no kind younger than the oldest may have \
1287					 been served in the same slice"
1288				);
1289			}
1290		}
1291	}
1292}