1use std::collections::VecDeque;
26use std::ops::Range;
27
28use roaring::RoaringBitmap;
29
30use super::BitmapBucketIteratorSource;
31use super::BitmapQuery;
32use super::BitmapScanBudget;
33use super::BitmapScanMetrics;
34use super::BucketItem;
35use super::DedupedQuery;
36use super::LeafHead;
37use super::LeafStop;
38use super::ObserveOnDrop;
39use super::ScanDirection;
40use super::ScanStop;
41use super::SkipPolicy;
42use super::WatermarkPacer;
43use super::WatermarkPacing;
44use super::Watermarked;
45use super::WatermarkedBucket;
46use super::advance_in_direction;
47use super::bound_in_direction;
48use super::bucket_edges;
49use super::build_term_specs;
50use super::collapse;
51use super::count_on_floor_refs;
52use super::eval_term_at_bucket;
53use super::frontier_advanced;
54use super::leaf_skip_targets;
55use super::recompute_unreferenced;
56use super::strictly_before;
57use super::take_snapshot_bitmap;
58
59struct Leaf<I> {
60 iter: I,
61 peeked: Option<Option<BucketItem>>,
62 drained: u64,
63}
64
65impl<I: Iterator<Item = BucketItem>> Leaf<I> {
66 fn new(iter: I) -> Self {
67 Self {
68 iter,
69 peeked: None,
70 drained: 0,
71 }
72 }
73
74 fn peek(&mut self) -> Option<&BucketItem> {
75 if self.peeked.is_none() {
76 self.peeked = Some(self.iter.next());
77 }
78 self.peeked.as_ref().and_then(Option::as_ref)
79 }
80
81 fn next(&mut self) -> Option<BucketItem> {
82 match self.peeked.take() {
83 Some(item) => item,
84 None => self.iter.next(),
85 }
86 }
87}
88
89impl<I: super::SeekableBucketIterator> Leaf<I> {
90 fn seek_bucket(&mut self, bucket: u64) -> bool {
94 let discarded_row = matches!(self.peeked.as_ref(), Some(Some(Ok(_))));
95 self.peeked = None;
96 self.iter.seek_bucket(bucket);
97 discarded_row
98 }
99}
100
101struct BudgetedBucketIter<I> {
108 inner: I,
109 budget: BitmapScanBudget,
110 first_bucket_pending: bool,
111 exhausted: bool,
112}
113
114impl<I> BudgetedBucketIter<I> {
115 fn new(inner: I, budget: BitmapScanBudget) -> Self {
116 Self {
117 inner,
118 budget,
119 first_bucket_pending: true,
120 exhausted: false,
121 }
122 }
123}
124
125impl<I: Iterator<Item = BucketItem>> Iterator for BudgetedBucketIter<I> {
126 type Item = BucketItem;
127
128 fn next(&mut self) -> Option<BucketItem> {
129 if self.exhausted {
130 return None;
131 }
132 let item = self.inner.next()?;
133 if item.is_err() {
134 return Some(item);
135 }
136 if self.first_bucket_pending {
137 self.budget.take_first();
138 self.first_bucket_pending = false;
139 } else if !self.budget.try_take() {
140 self.exhausted = true;
141 return Some(Err(LeafStop::BudgetExhausted));
142 }
143 Some(item)
144 }
145}
146
147impl<I: super::SeekableBucketIterator> super::SeekableBucketIterator for BudgetedBucketIter<I> {
148 fn seek_bucket(&mut self, bucket: u64) {
151 self.inner.seek_bucket(bucket);
152 }
153}
154
155struct PacedBucketIter<I> {
159 unpaced: I,
160 pacer: WatermarkPacer,
161 unpaced_ended: bool,
162}
163
164impl<I: Iterator<Item = WatermarkedBucket>> Iterator for PacedBucketIter<I> {
165 type Item = WatermarkedBucket;
166
167 fn next(&mut self) -> Option<WatermarkedBucket> {
168 while !self.unpaced_ended {
169 match self.unpaced.next() {
170 Some(output) => {
171 if let Some(paced) = self.pacer.pace(output) {
172 return Some(paced);
173 }
174 }
175 None => {
176 self.unpaced_ended = true;
177 return self
178 .pacer
179 .finish()
180 .map(|position| Ok(Watermarked::Watermark(position)));
181 }
182 }
183 }
184 None
185 }
186}
187
188pub fn eval_bitmap_query_bucket_iter<'a, S, F>(
207 source: S,
208 query: BitmapQuery,
209 range: Range<u64>,
210 bucket_size: u64,
211 direction: ScanDirection,
212 budget: u64,
213 policy: SkipPolicy,
214 pacing: WatermarkPacing,
215 on_metrics: F,
216) -> impl Iterator<Item = WatermarkedBucket> + 'a
217where
218 S: BitmapBucketIteratorSource<'a>,
219 F: FnOnce(BitmapScanMetrics) + 'a,
220{
221 let DedupedQuery {
224 keys: unique_keys,
225 mut terms,
226 } = build_term_specs(query.terms);
227 let budget = BitmapScanBudget::new(budget);
228 let mut leaves: Vec<Leaf<BudgetedBucketIter<S::Iter>>> = Vec::with_capacity(unique_keys.len());
229 for key in unique_keys {
230 leaves.push(Leaf::new(BudgetedBucketIter::new(
231 source.scan_bucket_iter(key, range.clone(), direction),
232 budget.clone(),
233 )));
234 }
235 let observer = ObserveOnDrop {
238 budget: budget.clone(),
239 callback: Some(on_metrics),
240 };
241 let pacer = WatermarkPacer::new(pacing, policy, budget.clone(), range.clone(), bucket_size);
242
243 let leaf_count = leaves.len();
244 let terminus = if direction.is_ascending() {
245 range.end
246 } else {
247 range.start
248 };
249 let request_floor = if direction.is_ascending() {
250 range.start
251 } else {
252 range.end
253 };
254 let mut unreferenced = vec![false; leaf_count];
257 let mut front = vec![request_floor; leaf_count];
261 let mut progress_frontier = Some(request_floor);
263 let mut done = false;
264 let mut pending: VecDeque<WatermarkedBucket> = VecDeque::new();
265
266 let unpaced = std::iter::from_fn(move || {
267 let _observer = &observer;
268 loop {
269 if let Some(out) = pending.pop_front() {
270 return Some(out);
271 }
272 if done {
273 return None;
274 }
275
276 let mut class: Vec<Option<LeafHead>> = (0..leaf_count).map(|_| None).collect();
279 for i in 0..leaf_count {
280 if unreferenced[i] {
281 continue;
282 }
283 match leaves[i].peek() {
284 Some(Ok((bucket, _))) => {
285 let (pre, _) = bucket_edges(*bucket, bucket_size, &range, direction);
286 front[i] = advance_in_direction(front[i], pre, direction);
287 class[i] = Some(LeafHead::Bucket(*bucket));
288 }
289 None => {
290 front[i] = advance_in_direction(front[i], terminus, direction);
291 class[i] = Some(LeafHead::Eof);
292 }
293 Some(Err(_)) => class[i] = Some(LeafHead::Error),
294 }
295 }
296
297 for term in terms.iter_mut() {
300 if !term.unsatisfiable
301 && term
302 .includes
303 .iter()
304 .any(|&i| matches!(class[i], Some(LeafHead::Eof)))
305 {
306 term.unsatisfiable = true;
307 }
308 }
309 recompute_unreferenced(&terms, &class, &mut unreferenced);
312
313 let targets = leaf_skip_targets(&terms, &class, &unreferenced, direction);
316 for (i, target) in targets.iter().enumerate() {
317 if let Some(target) = target {
318 let (pre, _) = bucket_edges(*target, bucket_size, &range, direction);
319 front[i] = advance_in_direction(front[i], pre, direction);
320 }
321 }
322
323 let mut errors: Vec<LeafStop> = Vec::new();
326 for i in 0..leaf_count {
327 if !unreferenced[i] && matches!(class[i], Some(LeafHead::Error)) {
328 match leaves[i].next() {
329 Some(Err(error)) => errors.push(error),
330 _ => unreachable!("peek classified Error"),
331 }
332 }
333 }
334
335 let active: Vec<usize> = (0..leaf_count).filter(|&i| !unreferenced[i]).collect();
336 if active.is_empty() {
339 done = true;
340 return None;
341 }
342
343 let floor_pos = active
346 .iter()
347 .map(|&i| front[i])
348 .reduce(|a, b| bound_in_direction(a, b, direction))
349 .expect("active non-empty");
350 let collapsed = (!errors.is_empty()).then(|| collapse(errors, floor_pos));
351 let scan_limited = matches!(collapsed, Some(ScanStop::ScanLimit { .. }));
352 if !scan_limited && frontier_advanced(progress_frontier, floor_pos, direction) {
353 pending.push_back(Ok(Watermarked::Watermark(floor_pos)));
354 progress_frontier = Some(floor_pos);
355 }
356 if let Some(stop) = collapsed {
357 done = true;
358 pending.push_back(Err(stop));
359 continue;
360 }
361
362 let lagging: Vec<usize> = active
365 .iter()
366 .copied()
367 .filter(|&i| targets[i].is_some())
368 .collect();
369 for i in 0..leaf_count {
372 if targets[i].is_none() {
373 leaves[i].drained = 0;
374 }
375 }
376
377 let eval_bucket = active
380 .iter()
381 .filter(|&&i| targets[i].is_none())
382 .filter_map(|&i| match class[i] {
383 Some(LeafHead::Bucket(bucket)) => Some(bucket),
384 _ => None,
385 })
386 .reduce(|a, b| bound_in_direction(a, b, direction))
387 .expect("a least term candidate leaf is not lagging");
388 let lagging_target = lagging
391 .iter()
392 .filter_map(|&i| targets[i])
393 .reduce(|a, b| bound_in_direction(a, b, direction));
394 let evaluate =
395 lagging_target.is_none_or(|target| strictly_before(eval_bucket, target, direction));
396
397 if evaluate {
398 let (_, post) = bucket_edges(eval_bucket, bucket_size, &range, direction);
399 let mut snapshot: Vec<Option<RoaringBitmap>> =
402 (0..leaf_count).map(|_| None).collect();
403 let mut on_floor = vec![false; leaf_count];
404 for i in 0..leaf_count {
405 if !unreferenced[i]
406 && targets[i].is_none()
407 && matches!(class[i], Some(LeafHead::Bucket(b)) if b == eval_bucket)
408 {
409 on_floor[i] = true;
410 front[i] = advance_in_direction(front[i], post, direction);
411 snapshot[i] = match leaves[i].next() {
412 Some(Ok((_, bitmap))) => Some(bitmap),
413 _ => None,
414 };
415 }
416 }
417 let mut remaining_refs = count_on_floor_refs(&terms, &on_floor);
420 let mut result: Option<RoaringBitmap> = None;
421 for term in &terms {
422 if term.unsatisfiable {
423 continue;
424 }
425 let includes = term
426 .includes
427 .iter()
428 .map(|&i| {
429 take_snapshot_bitmap(&mut snapshot, &mut remaining_refs, &on_floor, i)
430 })
431 .collect();
432 let excludes = term
433 .excludes
434 .iter()
435 .map(|&i| {
436 take_snapshot_bitmap(&mut snapshot, &mut remaining_refs, &on_floor, i)
437 })
438 .collect();
439 if let Some(bitmap) = eval_term_at_bucket(includes, excludes) {
440 result = Some(match result {
441 None => bitmap,
442 Some(acc) => acc | bitmap,
443 });
444 }
445 }
446 if let Some(bitmap) = result {
447 pending.push_back(Ok(Watermarked::Item((eval_bucket, bitmap))));
448 }
449 if frontier_advanced(progress_frontier, post, direction) {
450 pending.push_back(Ok(Watermarked::Watermark(post)));
451 progress_frontier = Some(post);
452 }
453 }
454
455 let seeks_before_catch_up = budget.seeks();
458 for i in lagging {
459 let target = targets[i].expect("lagging leaf has target");
460 loop {
461 let dead_row = matches!(
462 leaves[i].peek(),
463 Some(Ok((bucket, _))) if strictly_before(*bucket, target, direction)
464 );
465 if !dead_row {
466 break;
467 }
468 if policy
469 .drain_probe_rows
470 .is_some_and(|probe| leaves[i].drained >= u64::from(probe.get()))
471 {
472 if leaves[i].seek_bucket(target) {
475 budget.note_discarded();
476 }
477 budget.note_seek();
478 break;
479 }
480 let discarded = leaves[i].next();
483 debug_assert!(matches!(discarded, Some(Ok(_))));
484 leaves[i].drained += 1;
485 budget.note_discarded();
486 }
487 }
488 if budget.seeks() > seeks_before_catch_up {
489 budget.note_reopen_step();
490 }
491 }
492 });
493 PacedBucketIter {
494 unpaced,
495 pacer,
496 unpaced_ended: false,
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use std::collections::BTreeMap;
503 use std::sync::Arc;
504
505 use futures::StreamExt;
506
507 use super::*;
508 use crate::bitmap_query::BitmapBucketSource;
509 use crate::bitmap_query::BitmapTerm;
510 use crate::bitmap_query::eval_bitmap_query_bucket_stream;
511 use crate::bitmap_query::eval_bitmap_query_stream;
512 use crate::bitmap_query::flatten_watermarked_buckets;
513 use crate::bitmap_query::test_utils::*;
514
515 fn collect_marked(items: Vec<WatermarkedBucket>) -> Vec<Watermarked<(u64, Vec<u32>)>> {
518 items
519 .into_iter()
520 .map(|r| r.unwrap().map_item(|(b, bm)| (b, bm.iter().collect())))
521 .collect()
522 }
523
524 fn items_only(marked: &[Watermarked<(u64, Vec<u32>)>]) -> Vec<(u64, Vec<u32>)> {
525 marked
526 .iter()
527 .filter_map(|w| match w {
528 Watermarked::Item(it) => Some(it.clone()),
529 Watermarked::Watermark(_) => None,
530 })
531 .collect()
532 }
533
534 #[test]
535 fn eval_bitmap_query_bucket_iter_uses_iterator_source() {
536 let source = TestBucketSource {
537 buckets: Arc::new(BTreeMap::from([
538 (test_key(b"a"), vec![(0, vec![1, 2, 3]), (1, vec![5])]),
539 (test_key(b"b"), vec![(0, vec![2, 3]), (1, vec![5])]),
540 (test_key(b"c"), vec![(0, vec![3])]),
541 ])),
542 };
543 let query = BitmapQuery::new(vec![
544 BitmapTerm::new(vec![include(b"a"), include(b"b"), exclude(b"c")]).unwrap(),
545 ])
546 .unwrap();
547
548 let out = eval_bitmap_query_bucket_iter(
549 source,
550 query,
551 0..200_000,
552 BUCKET_SIZE,
553 ScanDirection::Ascending,
554 1_000_000,
555 SkipPolicy::DRAIN_ONLY,
556 EVERY_WATERMARK,
557 |_| {},
558 )
559 .collect::<Vec<_>>();
560 let out = items_only(&collect_marked(out));
561
562 assert_eq!(out, vec![(0, vec![2]), (1, vec![5])]);
563 }
564
565 #[test]
571 fn shared_include_across_terms_scans_dimension_once() {
572 use crate::bitmap_query::test_utils::CountingBucketSource;
573
574 let source = CountingBucketSource::new(BTreeMap::from([
575 (test_key(b"a"), vec![(0, vec![1, 2, 3])]),
576 (test_key(b"b"), vec![(0, vec![1])]),
577 (test_key(b"c"), vec![(0, vec![2])]),
578 ]));
579 let query = BitmapQuery::new(vec![
580 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
581 BitmapTerm::new(vec![include(b"a"), include(b"c")]).unwrap(),
582 ])
583 .unwrap();
584
585 let out = items_only(&collect_marked(
586 eval_bitmap_query_bucket_iter(
587 source.clone(),
588 query,
589 0..200_000,
590 BUCKET_SIZE,
591 ScanDirection::Ascending,
592 1_000_000,
593 SkipPolicy::DRAIN_ONLY,
594 EVERY_WATERMARK,
595 |_| {},
596 )
597 .collect(),
598 ));
599
600 assert_eq!(out, vec![(0, vec![1, 2])]);
602 assert_eq!(source.scan_count(&test_key(b"a")), 1);
603 assert_eq!(source.scan_count(&test_key(b"b")), 1);
604 assert_eq!(source.scan_count(&test_key(b"c")), 1);
605 }
606
607 #[tokio::test]
611 async fn eval_bitmap_query_bucket_iter_matches_stream_for_or_terms() {
612 let source = TestBucketSource {
613 buckets: Arc::new(BTreeMap::from([
614 (
615 test_key(b"a"),
616 vec![(0, vec![1, 2, 3]), (1, vec![5, 6]), (2, vec![9])],
617 ),
618 (
619 test_key(b"b"),
620 vec![(0, vec![2, 3]), (1, vec![6]), (2, vec![9, 10])],
621 ),
622 (test_key(b"c"), vec![(0, vec![3]), (2, vec![9])]),
623 (test_key(b"d"), vec![(1, vec![1, 8]), (2, vec![7])]),
624 (test_key(b"e"), vec![(1, vec![8])]),
625 ])),
626 };
627 let query = BitmapQuery::new(vec![
628 BitmapTerm::new(vec![include(b"a"), include(b"b"), exclude(b"c")]).unwrap(),
629 BitmapTerm::new(vec![include(b"d"), exclude(b"e")]).unwrap(),
630 ])
631 .unwrap();
632
633 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
634 for policy in [
635 SkipPolicy::DRAIN_ONLY,
636 SkipPolicy {
637 drain_probe_rows: std::num::NonZeroU32::new(2),
638 },
639 ] {
640 let stream_out: Vec<_> = eval_bitmap_query_bucket_stream(
641 source.clone(),
642 query.clone(),
643 0..300_000,
644 BUCKET_SIZE,
645 direction,
646 BitmapScanBudget::new(1_000_000),
647 policy,
648 )
649 .collect()
650 .await;
651 let iter_out: Vec<_> = eval_bitmap_query_bucket_iter(
652 source.clone(),
653 query.clone(),
654 0..300_000,
655 BUCKET_SIZE,
656 direction,
657 1_000_000,
658 policy,
659 EVERY_WATERMARK,
660 |_| {},
661 )
662 .collect();
663
664 assert_eq!(
665 collect_marked(stream_out),
666 collect_marked(iter_out),
667 "iter and stream marked sequences diverged for {direction:?}"
668 );
669 }
670 }
671 }
672
673 #[tokio::test]
677 async fn eval_bitmap_query_bucket_iter_matches_stream_over_sparse_gaps() {
678 let source = TestBucketSource {
679 buckets: Arc::new(BTreeMap::from([
680 (
681 test_key(b"a"),
682 vec![(0, vec![1, 2, 3]), (5, vec![5, 6]), (9, vec![9])],
683 ),
684 (
685 test_key(b"b"),
686 vec![(0, vec![2, 3]), (5, vec![6]), (9, vec![9, 10])],
687 ),
688 (test_key(b"c"), vec![(0, vec![3]), (9, vec![9])]),
689 (test_key(b"d"), vec![(3, vec![1, 8]), (7, vec![7])]),
690 (test_key(b"e"), vec![(3, vec![8])]),
691 ])),
692 };
693 let query = BitmapQuery::new(vec![
694 BitmapTerm::new(vec![include(b"a"), include(b"b"), exclude(b"c")]).unwrap(),
695 BitmapTerm::new(vec![include(b"d"), exclude(b"e")]).unwrap(),
696 ])
697 .unwrap();
698
699 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
700 for policy in [
701 SkipPolicy::DRAIN_ONLY,
702 SkipPolicy {
703 drain_probe_rows: std::num::NonZeroU32::new(2),
704 },
705 ] {
706 let stream_out: Vec<_> = eval_bitmap_query_bucket_stream(
707 source.clone(),
708 query.clone(),
709 0..(10 * BUCKET_SIZE),
710 BUCKET_SIZE,
711 direction,
712 BitmapScanBudget::new(1_000_000),
713 policy,
714 )
715 .collect()
716 .await;
717 let iter_out: Vec<_> = eval_bitmap_query_bucket_iter(
718 source.clone(),
719 query.clone(),
720 0..(10 * BUCKET_SIZE),
721 BUCKET_SIZE,
722 direction,
723 1_000_000,
724 policy,
725 EVERY_WATERMARK,
726 |_| {},
727 )
728 .collect();
729
730 assert_eq!(
731 collect_marked(stream_out),
732 collect_marked(iter_out),
733 "iter and stream diverged over sparse gaps for {direction:?}"
734 );
735 }
736 }
737 }
738
739 #[test]
742 fn intersect_emits_coalesced_watermarks_over_sparse_gap() {
743 let source = TestBucketSource {
744 buckets: Arc::new(BTreeMap::from([
745 (test_key(b"a"), vec![(0, vec![1]), (2, vec![5])]),
746 (test_key(b"b"), vec![(0, vec![1]), (2, vec![9])]),
747 ])),
748 };
749 let query = BitmapQuery::new(vec![
750 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
751 ])
752 .unwrap();
753
754 let marked = collect_marked(
755 eval_bitmap_query_bucket_iter(
756 source,
757 query,
758 0..300_000,
759 BUCKET_SIZE,
760 ScanDirection::Ascending,
761 1_000_000,
762 SkipPolicy::DRAIN_ONLY,
763 EVERY_WATERMARK,
764 |_| {},
765 )
766 .collect(),
767 );
768
769 assert_eq!(items_only(&marked), vec![(0, vec![1])]);
771
772 let watermarks: Vec<u64> = marked
774 .iter()
775 .filter_map(|w| match w {
776 Watermarked::Watermark(p) => Some(*p),
777 Watermarked::Item(_) => None,
778 })
779 .collect();
780 assert!(
781 watermarks.windows(2).all(|w| w[0] <= w[1]),
782 "ascending watermarks must be non-decreasing: {watermarks:?}"
783 );
784 assert_eq!(
785 watermarks.last().copied(),
786 Some(300_000),
787 "last bucket's post edge must reach the range terminus"
788 );
789 }
790
791 #[test]
792 fn descending_request_floor_is_not_emitted_as_progress() {
793 let source = TestBucketSource {
794 buckets: Arc::new(BTreeMap::from([(
795 test_key(b"a"),
796 vec![(2, vec![0, 50_000])],
797 )])),
798 };
799 let query = BitmapQuery::new(vec![BitmapTerm::new(vec![include(b"a")]).unwrap()]).unwrap();
800
801 let marked = collect_marked(
802 eval_bitmap_query_bucket_iter(
803 source,
804 query,
805 50..(2 * BUCKET_SIZE + 50_001),
806 BUCKET_SIZE,
807 ScanDirection::Descending,
808 1_000_000,
809 SkipPolicy::DRAIN_ONLY,
810 EVERY_WATERMARK,
811 |_| {},
812 )
813 .collect(),
814 );
815
816 assert_eq!(
817 marked,
818 vec![
819 Watermarked::Item((2, vec![0, 50_000])),
820 Watermarked::Watermark(2 * BUCKET_SIZE),
821 ],
822 );
823 }
824
825 #[test]
826 fn natural_completion_omits_terminus_but_retains_earned_progress() {
827 let source = TestBucketSource {
828 buckets: Arc::new(BTreeMap::from([(test_key(b"a"), vec![(3, vec![5])])])),
829 };
830 let query = BitmapQuery::new(vec![BitmapTerm::new(vec![include(b"a")]).unwrap()]).unwrap();
831
832 let marked = collect_marked(
833 eval_bitmap_query_bucket_iter(
834 source,
835 query,
836 0..(5 * BUCKET_SIZE),
837 BUCKET_SIZE,
838 ScanDirection::Ascending,
839 1_000_000,
840 SkipPolicy::DRAIN_ONLY,
841 EVERY_WATERMARK,
842 |_| {},
843 )
844 .collect(),
845 );
846
847 assert_eq!(
848 marked,
849 vec![
850 Watermarked::Watermark(3 * BUCKET_SIZE),
851 Watermarked::Item((3, vec![5])),
852 Watermarked::Watermark(4 * BUCKET_SIZE),
853 ],
854 );
855 }
856
857 #[test]
861 fn unanchored_term_emits_complement_over_gaps_and_past_exclude_eof() {
862 let source = TestBucketSource {
863 buckets: Arc::new(BTreeMap::from([(
864 test_key(b"x"),
865 vec![(0, vec![1, 2]), (2, vec![5])],
866 )])),
867 };
868 let query = BitmapQuery::new(vec![
869 BitmapTerm::new(vec![include_universe(), exclude(b"x")]).unwrap(),
870 ])
871 .unwrap();
872
873 let items: Vec<(u64, RoaringBitmap)> = eval_bitmap_query_bucket_iter(
874 source,
875 query,
876 0..(4 * BUCKET_SIZE),
877 BUCKET_SIZE,
878 ScanDirection::Ascending,
879 1_000_000,
880 SkipPolicy::DRAIN_ONLY,
881 EVERY_WATERMARK,
882 |_| {},
883 )
884 .filter_map(|r| match r.unwrap() {
885 Watermarked::Item(it) => Some(it),
886 Watermarked::Watermark(_) => None,
887 })
888 .collect();
889
890 let complement = |bits: &[u32]| {
891 let mut bm = full_bucket();
892 for &b in bits {
893 bm.remove(b);
894 }
895 bm
896 };
897 let expected = vec![
898 (0, complement(&[1, 2])),
899 (1, full_bucket()),
900 (2, complement(&[5])),
901 (3, full_bucket()),
902 ];
903 assert_eq!(items, expected);
904 }
905
906 #[tokio::test]
910 async fn eval_bitmap_query_bucket_iter_matches_stream_for_unanchored_terms() {
911 let source = TestBucketSource {
912 buckets: Arc::new(BTreeMap::from([
913 (test_key(b"a"), vec![(1, vec![7])]),
914 (test_key(b"x"), vec![(0, vec![1, 2]), (3, vec![5])]),
915 ])),
916 };
917 let query = BitmapQuery::new(vec![
918 BitmapTerm::new(vec![include(b"a")]).unwrap(),
919 BitmapTerm::new(vec![include_universe(), exclude(b"x")]).unwrap(),
920 ])
921 .unwrap();
922
923 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
924 for policy in [
925 SkipPolicy::DRAIN_ONLY,
926 SkipPolicy {
927 drain_probe_rows: std::num::NonZeroU32::new(2),
928 },
929 ] {
930 let stream_out: Vec<_> = eval_bitmap_query_bucket_stream(
931 source.clone(),
932 query.clone(),
933 0..(5 * BUCKET_SIZE),
934 BUCKET_SIZE,
935 direction,
936 BitmapScanBudget::new(1_000_000),
937 policy,
938 )
939 .collect()
940 .await;
941 let iter_out: Vec<_> = eval_bitmap_query_bucket_iter(
942 source.clone(),
943 query.clone(),
944 0..(5 * BUCKET_SIZE),
945 BUCKET_SIZE,
946 direction,
947 1_000_000,
948 policy,
949 EVERY_WATERMARK,
950 |_| {},
951 )
952 .collect();
953
954 assert_eq!(
955 collect_marked(stream_out),
956 collect_marked(iter_out),
957 "iter and stream diverged on unanchored terms for {direction:?}"
958 );
959 }
960 }
961 }
962
963 #[tokio::test]
967 async fn unanchored_budget_exhaustion_resumes_at_terminal_frontier() {
968 let source = TestBucketSource {
969 buckets: Arc::new(BTreeMap::new()),
970 };
971 let query = BitmapQuery::new(vec![
972 BitmapTerm::new(vec![include_universe(), exclude(b"x")]).unwrap(),
973 ])
974 .unwrap();
975
976 let first: Vec<_> = eval_bitmap_query_bucket_stream(
977 source.clone(),
978 query.clone(),
979 0..(10 * BUCKET_SIZE),
980 BUCKET_SIZE,
981 ScanDirection::Ascending,
982 BitmapScanBudget::new(3),
983 SkipPolicy::DRAIN_ONLY,
984 )
985 .collect()
986 .await;
987
988 let mut covered: Vec<u64> = Vec::new();
989 let mut resume_from = None;
990 let mut limit_hit = false;
991 for item in first {
992 match item {
993 Ok(Watermarked::Item((bucket, bitmap))) => {
994 assert_eq!(bitmap, full_bucket());
995 covered.push(bucket);
996 }
997 Ok(Watermarked::Watermark(_)) => {}
998 Err(ScanStop::ScanLimit { scan_frontier }) => {
999 resume_from = Some(scan_frontier);
1000 limit_hit = true;
1001 }
1002 Err(other) => panic!("expected ScanLimit, got {other:?}"),
1003 }
1004 }
1005 assert!(limit_hit, "3-bucket budget cannot cover 10 dense buckets");
1006 assert_eq!(covered, vec![0, 1, 2]);
1007 let resume_from = resume_from.expect("ScanLimit must carry a resume frontier");
1008 assert_eq!(resume_from, 3 * BUCKET_SIZE);
1009
1010 let resumed: Vec<_> = eval_bitmap_query_bucket_stream(
1011 source,
1012 query,
1013 resume_from..(10 * BUCKET_SIZE),
1014 BUCKET_SIZE,
1015 ScanDirection::Ascending,
1016 BitmapScanBudget::new(1_000_000),
1017 SkipPolicy::DRAIN_ONLY,
1018 )
1019 .collect()
1020 .await;
1021 for item in resumed {
1022 if let Watermarked::Item((bucket, _)) = item.unwrap() {
1023 covered.push(bucket);
1024 }
1025 }
1026 assert_eq!(covered, (0..10).collect::<Vec<_>>());
1027 }
1028 #[tokio::test]
1029 async fn iter_seeks_lagging_leaf_natively() {
1030 let source = CountingBucketSource::new(BTreeMap::from([
1031 (test_key(b"a"), vec![(0, vec![1]), (50, vec![1])]),
1032 (
1033 test_key(b"b"),
1034 (0..=50).map(|bucket| (bucket, vec![1])).collect(),
1035 ),
1036 ]));
1037 let query = BitmapQuery::new(vec![
1038 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
1039 ])
1040 .unwrap();
1041 let policy = SkipPolicy {
1042 drain_probe_rows: std::num::NonZeroU32::new(2),
1043 };
1044
1045 let stream_out = eval_bitmap_query_bucket_stream(
1046 source.clone(),
1047 query.clone(),
1048 0..(51 * BUCKET_SIZE),
1049 BUCKET_SIZE,
1050 ScanDirection::Ascending,
1051 BitmapScanBudget::new(1_000),
1052 policy,
1053 )
1054 .collect()
1055 .await;
1056 let iter_out = eval_bitmap_query_bucket_iter(
1057 source.clone(),
1058 query,
1059 0..(51 * BUCKET_SIZE),
1060 BUCKET_SIZE,
1061 ScanDirection::Ascending,
1062 1_000,
1063 policy,
1064 EVERY_WATERMARK,
1065 |_| {},
1066 )
1067 .collect();
1068
1069 assert_eq!(collect_marked(stream_out), collect_marked(iter_out));
1070 assert_eq!(source.seek_count(&test_key(b"b")), 1);
1071 }
1072
1073 type ObservedOutput = Result<Watermarked<(u64, Vec<u32>)>, u64>;
1076
1077 fn observe(outputs: Vec<WatermarkedBucket>) -> Vec<ObservedOutput> {
1079 outputs
1080 .into_iter()
1081 .map(|output| match output {
1082 Ok(marked) => {
1083 Ok(marked.map_item(|(bucket, bitmap)| (bucket, bitmap.iter().collect())))
1084 }
1085 Err(ScanStop::ScanLimit { scan_frontier }) => Err(scan_frontier),
1086 Err(other) => panic!("expected only scan-limit stops, got {other:?}"),
1087 })
1088 .collect()
1089 }
1090
1091 async fn assert_iter_matches_stream_under_budgets(
1096 source: TestBucketSource,
1097 query: BitmapQuery,
1098 range: Range<u64>,
1099 budgets: std::ops::RangeInclusive<u64>,
1100 ) {
1101 let mut stopped = 0;
1102 let mut completed = 0;
1103 for budget in budgets {
1104 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
1105 for policy in [
1106 SkipPolicy::DRAIN_ONLY,
1107 SkipPolicy {
1108 drain_probe_rows: std::num::NonZeroU32::new(2),
1109 },
1110 ] {
1111 let stream_out = observe(
1112 eval_bitmap_query_bucket_stream(
1113 source.clone(),
1114 query.clone(),
1115 range.clone(),
1116 BUCKET_SIZE,
1117 direction,
1118 BitmapScanBudget::new(budget),
1119 policy,
1120 )
1121 .collect()
1122 .await,
1123 );
1124 let iter_out = observe(
1125 eval_bitmap_query_bucket_iter(
1126 source.clone(),
1127 query.clone(),
1128 range.clone(),
1129 BUCKET_SIZE,
1130 direction,
1131 budget,
1132 policy,
1133 EVERY_WATERMARK,
1134 |_| {},
1135 )
1136 .collect(),
1137 );
1138
1139 assert_eq!(
1140 stream_out, iter_out,
1141 "iter and stream diverged with budget {budget}, {direction:?}, {policy:?}"
1142 );
1143 if matches!(iter_out.last(), Some(Err(_))) {
1144 stopped += 1;
1145 } else {
1146 completed += 1;
1147 }
1148 }
1149 }
1150 }
1151 assert!(stopped > 0, "no budget in the sweep exhausted the scan");
1152 assert!(completed > 0, "no budget in the sweep completed the scan");
1153 }
1154
1155 fn skewed_and_source() -> TestBucketSource {
1158 TestBucketSource {
1159 buckets: Arc::new(BTreeMap::from([
1160 (test_key(b"a"), vec![(0, vec![1]), (50, vec![1])]),
1161 (
1162 test_key(b"b"),
1163 (0..=50).map(|bucket| (bucket, vec![1])).collect(),
1164 ),
1165 ])),
1166 }
1167 }
1168
1169 fn skewed_and_query() -> BitmapQuery {
1170 BitmapQuery::new(vec![
1171 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
1172 ])
1173 .unwrap()
1174 }
1175
1176 #[tokio::test]
1179 async fn small_budget_iter_matches_stream_during_gap_catch_up() {
1180 assert_iter_matches_stream_under_budgets(
1181 skewed_and_source(),
1182 skewed_and_query(),
1183 0..(51 * BUCKET_SIZE),
1184 1..=60,
1185 )
1186 .await;
1187 }
1188
1189 #[tokio::test]
1192 async fn small_budget_iter_matches_stream_for_or_terms_over_sparse_gaps() {
1193 let source = TestBucketSource {
1194 buckets: Arc::new(BTreeMap::from([
1195 (
1196 test_key(b"a"),
1197 vec![(0, vec![1, 2, 3]), (5, vec![5, 6]), (9, vec![9])],
1198 ),
1199 (
1200 test_key(b"b"),
1201 vec![(0, vec![2, 3]), (5, vec![6]), (9, vec![9, 10])],
1202 ),
1203 (test_key(b"c"), vec![(0, vec![3]), (9, vec![9])]),
1204 (test_key(b"d"), vec![(3, vec![1, 8]), (7, vec![7])]),
1205 (test_key(b"e"), vec![(3, vec![8])]),
1206 ])),
1207 };
1208 let query = BitmapQuery::new(vec![
1209 BitmapTerm::new(vec![include(b"a"), include(b"b"), exclude(b"c")]).unwrap(),
1210 BitmapTerm::new(vec![include(b"d"), exclude(b"e")]).unwrap(),
1211 ])
1212 .unwrap();
1213
1214 assert_iter_matches_stream_under_budgets(source, query, 0..(10 * BUCKET_SIZE), 1..=20)
1215 .await;
1216 }
1217
1218 #[test]
1223 fn budget_at_or_below_leaf_count_still_reports_every_first_position() {
1224 let source = TestBucketSource {
1225 buckets: Arc::new(BTreeMap::from([
1226 (
1227 test_key(b"a"),
1228 (0..=4).map(|bucket| (bucket, vec![1])).collect(),
1229 ),
1230 (test_key(b"b"), vec![(50, vec![1])]),
1231 (test_key(b"c"), vec![(40, vec![7])]),
1232 ])),
1233 };
1234 let query = BitmapQuery::new(vec![
1235 BitmapTerm::new(vec![include(b"a"), include(b"b")]).unwrap(),
1236 BitmapTerm::new(vec![include(b"c")]).unwrap(),
1237 ])
1238 .unwrap();
1239
1240 for budget in 1..=3 {
1241 let out = observe(
1242 eval_bitmap_query_bucket_iter(
1243 source.clone(),
1244 query.clone(),
1245 0..(60 * BUCKET_SIZE),
1246 BUCKET_SIZE,
1247 ScanDirection::Ascending,
1248 budget,
1249 SkipPolicy::DRAIN_ONLY,
1250 EVERY_WATERMARK,
1251 |_| {},
1252 )
1253 .collect(),
1254 );
1255
1256 assert_eq!(
1257 out,
1258 vec![
1259 Ok(Watermarked::Watermark(40 * BUCKET_SIZE)),
1260 Ok(Watermarked::Item((40, vec![7]))),
1261 Ok(Watermarked::Watermark(41 * BUCKET_SIZE)),
1262 Err(50 * BUCKET_SIZE),
1263 ],
1264 "budget {budget}"
1265 );
1266 }
1267 }
1268
1269 #[tokio::test]
1272 async fn metrics_match_stream_when_crossing_gap() {
1273 let policy = SkipPolicy {
1274 drain_probe_rows: std::num::NonZeroU32::new(2),
1275 };
1276 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
1277 let (stream_tx, stream_rx) = std::sync::mpsc::channel();
1278 let _: Vec<_> = eval_bitmap_query_stream(
1279 skewed_and_source(),
1280 skewed_and_query(),
1281 0..(51 * BUCKET_SIZE),
1282 BUCKET_SIZE,
1283 direction,
1284 1_000,
1285 policy,
1286 EVERY_WATERMARK,
1287 move |observed| stream_tx.send(observed).unwrap(),
1288 )
1289 .collect()
1290 .await;
1291 let (iter_tx, iter_rx) = std::sync::mpsc::channel();
1292 let _: Vec<_> = eval_bitmap_query_bucket_iter(
1293 skewed_and_source(),
1294 skewed_and_query(),
1295 0..(51 * BUCKET_SIZE),
1296 BUCKET_SIZE,
1297 direction,
1298 1_000,
1299 policy,
1300 EVERY_WATERMARK,
1301 move |observed| iter_tx.send(observed).unwrap(),
1302 )
1303 .collect();
1304
1305 let iter_metrics = iter_rx.recv().expect("iter metrics reported");
1306 assert_eq!(
1307 iter_metrics,
1308 stream_rx.recv().expect("stream metrics reported"),
1309 "{direction:?}"
1310 );
1311 assert_eq!(
1312 iter_metrics,
1313 BitmapScanMetrics {
1314 buckets_evaluated: 7,
1315 buckets_discarded: 3,
1316 leaf_seeks: 1,
1317 },
1318 "{direction:?}"
1319 );
1320 }
1321 }
1322
1323 #[test]
1326 fn metrics_fire_once_when_dropped_mid_scan() {
1327 fn scan(
1328 on_metrics: impl FnOnce(BitmapScanMetrics) + 'static,
1329 ) -> impl Iterator<Item = WatermarkedBucket> {
1330 eval_bitmap_query_bucket_iter(
1331 skewed_and_source(),
1332 skewed_and_query(),
1333 0..(51 * BUCKET_SIZE),
1334 BUCKET_SIZE,
1335 ScanDirection::Ascending,
1336 1_000,
1337 SkipPolicy {
1338 drain_probe_rows: std::num::NonZeroU32::new(2),
1339 },
1340 EVERY_WATERMARK,
1341 on_metrics,
1342 )
1343 }
1344
1345 let (unpolled_tx, unpolled_rx) = std::sync::mpsc::channel();
1346 let unpolled = scan(move |observed| unpolled_tx.send(observed).unwrap());
1347 assert!(unpolled_rx.try_recv().is_err(), "fired before drop");
1348 drop(unpolled);
1349 assert_eq!(
1350 unpolled_rx.try_recv().expect("fired on drop"),
1351 BitmapScanMetrics {
1352 buckets_evaluated: 0,
1353 buckets_discarded: 0,
1354 leaf_seeks: 0,
1355 }
1356 );
1357 assert!(
1358 matches!(
1359 unpolled_rx.try_recv(),
1360 Err(std::sync::mpsc::TryRecvError::Disconnected)
1361 ),
1362 "fired more than once"
1363 );
1364
1365 let (partial_tx, partial_rx) = std::sync::mpsc::channel();
1366 let mut partial = scan(move |observed| partial_tx.send(observed).unwrap());
1367 assert!(matches!(
1370 partial.next(),
1371 Some(Ok(Watermarked::Item((0, _))))
1372 ));
1373 assert!(partial_rx.try_recv().is_err(), "fired before drop");
1374 drop(partial);
1375 assert_eq!(
1376 partial_rx.try_recv().expect("fired on drop"),
1377 BitmapScanMetrics {
1378 buckets_evaluated: 2,
1379 buckets_discarded: 0,
1380 leaf_seeks: 0,
1381 }
1382 );
1383 assert!(
1384 matches!(
1385 partial_rx.try_recv(),
1386 Err(std::sync::mpsc::TryRecvError::Disconnected)
1387 ),
1388 "fired more than once"
1389 );
1390 }
1391
1392 #[test]
1397 fn absent_include_annihilates_term() {
1398 let source = TestBucketSource {
1399 buckets: Arc::new(BTreeMap::from([(test_key(b"a"), vec![(0, vec![1, 2])])])),
1400 };
1401 let query =
1402 BitmapQuery::new(vec![BitmapTerm::new(vec![include(b"ghost")]).unwrap()]).unwrap();
1403
1404 let out = eval_bitmap_query_bucket_iter(
1405 source,
1406 query,
1407 0..200_000,
1408 BUCKET_SIZE,
1409 ScanDirection::Ascending,
1410 1_000_000,
1411 SkipPolicy::DRAIN_ONLY,
1412 EVERY_WATERMARK,
1413 |_| {},
1414 )
1415 .collect::<Vec<_>>();
1416 let items = items_only(&collect_marked(out));
1417
1418 assert!(
1419 items.is_empty(),
1420 "absent include must annihilate: {items:?}"
1421 );
1422 }
1423
1424 #[test]
1427 fn absent_include_annihilates_term_despite_present_include() {
1428 let source = TestBucketSource {
1429 buckets: Arc::new(BTreeMap::from([(test_key(b"a"), vec![(0, vec![1, 2])])])),
1430 };
1431 let query = BitmapQuery::new(vec![
1432 BitmapTerm::new(vec![include(b"a"), include(b"ghost")]).unwrap(),
1433 ])
1434 .unwrap();
1435
1436 let out = eval_bitmap_query_bucket_iter(
1437 source,
1438 query,
1439 0..200_000,
1440 BUCKET_SIZE,
1441 ScanDirection::Ascending,
1442 1_000_000,
1443 SkipPolicy::DRAIN_ONLY,
1444 EVERY_WATERMARK,
1445 |_| {},
1446 )
1447 .collect::<Vec<_>>();
1448 let items = items_only(&collect_marked(out));
1449
1450 assert!(
1451 items.is_empty(),
1452 "absent include must annihilate: {items:?}"
1453 );
1454 }
1455
1456 #[test]
1459 fn absent_exclude_is_noop() {
1460 let source = TestBucketSource {
1461 buckets: Arc::new(BTreeMap::from([(test_key(b"a"), vec![(0, vec![1, 2])])])),
1462 };
1463 let query = BitmapQuery::new(vec![
1464 BitmapTerm::new(vec![include(b"a"), exclude(b"ghost")]).unwrap(),
1465 ])
1466 .unwrap();
1467
1468 let out = eval_bitmap_query_bucket_iter(
1469 source,
1470 query,
1471 0..200_000,
1472 BUCKET_SIZE,
1473 ScanDirection::Ascending,
1474 1_000_000,
1475 SkipPolicy::DRAIN_ONLY,
1476 EVERY_WATERMARK,
1477 |_| {},
1478 )
1479 .collect::<Vec<_>>();
1480
1481 assert_eq!(items_only(&collect_marked(out)), vec![(0, vec![1, 2])]);
1482 }
1483
1484 async fn assert_paced_iter_matches_paced_stream<S>(source: S) -> usize
1489 where
1490 S: BitmapBucketSource + for<'a> BitmapBucketIteratorSource<'a>,
1491 {
1492 let range = 0..(10 * BUCKET_SIZE);
1493 let mut scan_limited = 0;
1494 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
1495 for budget in [7, 1_000_000] {
1496 for pacing in [
1497 charged_buckets(1, false),
1498 charged_buckets(3, true),
1499 charged_buckets(5, false),
1500 charged_buckets(5, true),
1501 ] {
1502 let iter_buckets: Vec<_> = eval_bitmap_query_bucket_iter(
1503 source.clone(),
1504 parallel_leaves_query(),
1505 range.clone(),
1506 BUCKET_SIZE,
1507 direction,
1508 budget,
1509 SkipPolicy::DRAIN_ONLY,
1510 pacing,
1511 |_| {},
1512 )
1513 .collect();
1514 let iter_members: Vec<_> = flatten_watermarked_buckets(
1515 futures::stream::iter(iter_buckets),
1516 range.clone(),
1517 BUCKET_SIZE,
1518 direction,
1519 )
1520 .collect()
1521 .await;
1522 let stream_members: Vec<_> = eval_bitmap_query_stream(
1523 source.clone(),
1524 parallel_leaves_query(),
1525 range.clone(),
1526 BUCKET_SIZE,
1527 direction,
1528 budget,
1529 SkipPolicy::DRAIN_ONLY,
1530 pacing,
1531 |_| {},
1532 )
1533 .collect()
1534 .await;
1535
1536 let iter_members = observe_member_outputs(iter_members);
1537 assert_eq!(
1538 iter_members,
1539 observe_member_outputs(stream_members),
1540 "paced iter and stream diverged with budget {budget}, {direction:?}, \
1541 {pacing:?}"
1542 );
1543 if matches!(iter_members.last(), Some(Err(ObservedStop::ScanLimit(_)))) {
1544 scan_limited += 1;
1545 }
1546 }
1547 }
1548 }
1549 scan_limited
1550 }
1551
1552 #[tokio::test]
1555 async fn paced_iter_matches_paced_stream() {
1556 let scan_limited =
1557 assert_paced_iter_matches_paced_stream(parallel_leaves_source(10, &[3, 6])).await;
1558 assert!(scan_limited > 0, "no run exhausted the budget");
1559 assert_paced_iter_matches_paced_stream(StoppingBucketSource {
1560 inner: parallel_leaves_source(10, &[3]),
1561 stop_key: test_key(b"a"),
1562 rows_before_stop: 5,
1563 stop: || LeafStop::Fault(anyhow::anyhow!("leaf storage fault")),
1564 })
1565 .await;
1566 }
1567
1568 #[derive(Clone, Copy, Debug)]
1570 enum Evaluator {
1571 Iter,
1572 Stream,
1573 }
1574
1575 #[derive(Clone, Copy, Debug)]
1578 struct ScanCounts {
1579 rows: u64,
1580 reopens: u64,
1581 }
1582
1583 impl ScanCounts {
1584 fn work(self, policy: SkipPolicy) -> u64 {
1588 self.rows + policy.reopen_cost_rows() * self.reopens
1589 }
1590 }
1591
1592 struct ChargedRun {
1595 outputs: Vec<(ObservedMemberOutput, ScanCounts)>,
1596 metrics: BitmapScanMetrics,
1597 }
1598
1599 impl ChargedRun {
1600 fn member_outputs(&self) -> Vec<ObservedMemberOutput> {
1601 self.outputs
1602 .iter()
1603 .map(|(output, _)| output.clone())
1604 .collect()
1605 }
1606
1607 fn items(&self) -> Vec<u64> {
1608 self.outputs
1609 .iter()
1610 .filter_map(|(output, _)| match output {
1611 Ok(Watermarked::Item(member)) => Some(*member),
1612 _ => None,
1613 })
1614 .collect()
1615 }
1616
1617 fn watermarks(&self) -> Vec<u64> {
1618 self.outputs
1619 .iter()
1620 .filter_map(|(output, _)| match output {
1621 Ok(Watermarked::Watermark(position)) => Some(*position),
1622 _ => None,
1623 })
1624 .collect()
1625 }
1626 }
1627
1628 async fn charged_member_run(
1633 evaluator: Evaluator,
1634 source: TestBucketSource,
1635 query: BitmapQuery,
1636 range: Range<u64>,
1637 direction: ScanDirection,
1638 policy: SkipPolicy,
1639 pacing: WatermarkPacing,
1640 ) -> ChargedRun {
1641 let source = ScanWorkCountingSource::new(source);
1642 let counts = |source: &ScanWorkCountingSource| ScanCounts {
1643 rows: source.rows_yielded(),
1644 reopens: source.reopens(),
1645 };
1646 let (metrics_tx, metrics_rx) = std::sync::mpsc::channel();
1647 let on_metrics = move |metrics: BitmapScanMetrics| metrics_tx.send(metrics).unwrap();
1648 let mut outputs = Vec::new();
1649 match evaluator {
1650 Evaluator::Iter => {
1651 let bucket_outputs = eval_bitmap_query_bucket_iter(
1652 source.clone(),
1653 query,
1654 range.clone(),
1655 BUCKET_SIZE,
1656 direction,
1657 1_000_000,
1658 policy,
1659 pacing,
1660 on_metrics,
1661 );
1662 for bucket_output in bucket_outputs {
1663 let counted = counts(&source);
1664 let members: Vec<_> = flatten_watermarked_buckets(
1665 futures::stream::iter([bucket_output]),
1666 range.clone(),
1667 BUCKET_SIZE,
1668 direction,
1669 )
1670 .collect()
1671 .await;
1672 outputs.extend(
1673 observe_member_outputs(members)
1674 .into_iter()
1675 .map(|output| (output, counted)),
1676 );
1677 }
1678 }
1679 Evaluator::Stream => {
1680 let mut member_outputs = eval_bitmap_query_stream(
1681 source.clone(),
1682 query,
1683 range,
1684 BUCKET_SIZE,
1685 direction,
1686 1_000_000,
1687 policy,
1688 pacing,
1689 on_metrics,
1690 );
1691 while let Some(output) = member_outputs.next().await {
1692 let counted = counts(&source);
1693 outputs.extend(
1694 observe_member_outputs(vec![output])
1695 .into_iter()
1696 .map(|output| (output, counted)),
1697 );
1698 }
1699 }
1700 }
1701 let metrics = metrics_rx.recv().unwrap();
1702 assert_eq!(
1703 metrics.buckets_evaluated,
1704 source.rows_yielded(),
1705 "{evaluator:?}: every yielded row must be charged"
1706 );
1707 assert_eq!(
1708 metrics.leaf_seeks,
1709 source.reopens(),
1710 "{evaluator:?}: every reopen must be counted"
1711 );
1712 ChargedRun { outputs, metrics }
1713 }
1714
1715 fn watermark_at_bucket(bucket: u64) -> ObservedMemberOutput {
1716 Ok(Watermarked::Watermark(bucket * BUCKET_SIZE))
1717 }
1718
1719 #[tokio::test]
1729 async fn edge_item_without_in_range_members_does_not_delay_watermarks() {
1730 let cases = [
1731 (
1732 ScanDirection::Ascending,
1733 parallel_leaves_source(10, &[0]),
1734 2..(10 * BUCKET_SIZE),
1735 [1, 4, 7, 10],
1736 ),
1737 (
1738 ScanDirection::Descending,
1739 parallel_leaves_source(10, &[9]),
1740 0..(9 * BUCKET_SIZE + 1),
1741 [9, 6, 3, 0],
1742 ),
1743 ];
1744 for evaluator in [Evaluator::Iter, Evaluator::Stream] {
1745 for (direction, source, range, emitted_buckets) in cases.clone() {
1746 let context = format!("{evaluator:?}, {direction:?}");
1747 let expected: Vec<ObservedMemberOutput> = emitted_buckets
1748 .into_iter()
1749 .map(watermark_at_bucket)
1750 .collect();
1751 let unpaced = charged_member_run(
1752 evaluator,
1753 source.clone(),
1754 parallel_leaves_query(),
1755 range.clone(),
1756 direction,
1757 SkipPolicy::DRAIN_ONLY,
1758 EVERY_WATERMARK,
1759 )
1760 .await;
1761 assert_eq!(
1762 unpaced.member_outputs().first(),
1763 expected.first(),
1764 "{context}"
1765 );
1766 let paced = charged_member_run(
1767 evaluator,
1768 source,
1769 parallel_leaves_query(),
1770 range,
1771 direction,
1772 SkipPolicy::DRAIN_ONLY,
1773 charged_buckets(5, false),
1774 )
1775 .await;
1776 assert_eq!(paced.member_outputs(), expected, "{context}");
1777 }
1778 }
1779 }
1780
1781 #[tokio::test]
1789 async fn edge_item_with_in_range_member_restarts_count() {
1790 let cases = [
1791 (
1792 ScanDirection::Ascending,
1793 parallel_leaves_source_with_matches(10, &[(0, &[1, 3][..])]),
1794 2..(10 * BUCKET_SIZE),
1795 3,
1796 [4, 7, 10],
1797 ),
1798 (
1799 ScanDirection::Descending,
1800 parallel_leaves_source_with_matches(10, &[(9, &[1, 3][..])]),
1801 0..(9 * BUCKET_SIZE + 2),
1802 9 * BUCKET_SIZE + 1,
1803 [6, 3, 0],
1804 ),
1805 ];
1806 for evaluator in [Evaluator::Iter, Evaluator::Stream] {
1807 for (direction, source, range, in_range_member, emitted_buckets) in cases.clone() {
1808 let mut expected: Vec<ObservedMemberOutput> =
1809 vec![Ok(Watermarked::Item(in_range_member))];
1810 expected.extend(emitted_buckets.into_iter().map(watermark_at_bucket));
1811 let paced = charged_member_run(
1812 evaluator,
1813 source,
1814 parallel_leaves_query(),
1815 range,
1816 direction,
1817 SkipPolicy::DRAIN_ONLY,
1818 charged_buckets(5, false),
1819 )
1820 .await;
1821 assert_eq!(
1822 paced.member_outputs(),
1823 expected,
1824 "{evaluator:?}, {direction:?}"
1825 );
1826 }
1827 }
1828 }
1829
1830 fn sparse_and_dense_source() -> TestBucketSource {
1835 TestBucketSource {
1836 buckets: Arc::new(BTreeMap::from([
1837 (
1838 test_key(b"a"),
1839 (0..=40)
1840 .step_by(10)
1841 .map(|bucket| (bucket, vec![1]))
1842 .collect(),
1843 ),
1844 (
1845 test_key(b"b"),
1846 (0..=40)
1847 .map(|bucket| {
1848 if bucket == 20 {
1849 (bucket, vec![1, 2])
1850 } else {
1851 (bucket, vec![2])
1852 }
1853 })
1854 .collect(),
1855 ),
1856 ])),
1857 }
1858 }
1859
1860 #[tokio::test]
1866 async fn charged_buckets_paces_by_scan_work_during_gap_catch_up() {
1867 const MIN_BUCKETS: u64 = 3;
1868 let range = 0..(41 * BUCKET_SIZE);
1869 for evaluator in [Evaluator::Iter, Evaluator::Stream] {
1870 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
1871 for policy in [
1872 SkipPolicy::DRAIN_ONLY,
1873 SkipPolicy {
1874 drain_probe_rows: std::num::NonZeroU32::new(2),
1875 },
1876 ] {
1877 let context = format!("{evaluator:?}, {direction:?}, {policy:?}");
1878 let unpaced = charged_member_run(
1879 evaluator,
1880 sparse_and_dense_source(),
1881 skewed_and_query(),
1882 range.clone(),
1883 direction,
1884 policy,
1885 EVERY_WATERMARK,
1886 )
1887 .await;
1888 let paced = charged_member_run(
1889 evaluator,
1890 sparse_and_dense_source(),
1891 skewed_and_query(),
1892 range.clone(),
1893 direction,
1894 policy,
1895 charged_buckets(MIN_BUCKETS, false),
1896 )
1897 .await;
1898
1899 assert!(paced.metrics.buckets_discarded > 0, "{context}");
1902 assert_eq!(
1903 paced.metrics.leaf_seeks > 0,
1904 policy.drain_probe_rows.is_some(),
1905 "{context}"
1906 );
1907
1908 assert_eq!(unpaced.items(), vec![20 * BUCKET_SIZE + 1], "{context}");
1909 assert_eq!(paced.items(), unpaced.items(), "{context}");
1910 assert_eq!(
1911 paced.outputs.first().map(|(output, _)| output),
1912 unpaced.outputs.first().map(|(output, _)| output),
1913 "first output must not be held: {context}"
1914 );
1915
1916 let paced_watermarks = paced.watermarks();
1917 let unpaced_watermarks = unpaced.watermarks();
1918 let mut unmatched_unpaced = unpaced_watermarks.iter();
1919 for position in &paced_watermarks {
1920 assert!(
1921 unmatched_unpaced.any(|candidate| candidate == position),
1922 "paced {paced_watermarks:?} is not a subsequence of unpaced \
1923 {unpaced_watermarks:?}: {context}"
1924 );
1925 }
1926 assert!(
1929 paced_watermarks.len() < unpaced_watermarks.len(),
1930 "no watermark held: {context}"
1931 );
1932 assert!(
1933 paced_watermarks.len() >= 2,
1934 "no watermark after the first emitted: {context}"
1935 );
1936
1937 for pair in paced.outputs.windows(2) {
1938 let [(_, previous), (output, current)] = pair else {
1939 unreachable!("windows(2) yields pairs");
1940 };
1941 if matches!(output, Ok(Watermarked::Watermark(_))) {
1942 let work = current.work(policy) - previous.work(policy);
1943 assert!(
1944 work >= MIN_BUCKETS,
1945 "{output:?} emitted {work} of scan work after the previous \
1946 emission: {context}"
1947 );
1948 }
1949 }
1950 }
1951 }
1952 }
1953 }
1954
1955 #[tokio::test]
1962 async fn both_evaluators_pace_by_rows_and_reopen_steps() {
1963 let range = 0..(41 * BUCKET_SIZE);
1964 let policy = SkipPolicy {
1965 drain_probe_rows: std::num::NonZeroU32::new(2),
1966 };
1967 for evaluator in [Evaluator::Iter, Evaluator::Stream] {
1968 for direction in [ScanDirection::Ascending, ScanDirection::Descending] {
1969 let unpaced = charged_member_run(
1970 evaluator,
1971 sparse_and_dense_source(),
1972 skewed_and_query(),
1973 range.clone(),
1974 direction,
1975 policy,
1976 EVERY_WATERMARK,
1977 )
1978 .await;
1979 assert!(
1980 unpaced.metrics.leaf_seeks > 0,
1981 "{evaluator:?}, {direction:?}"
1982 );
1983 let mut due_only_to_reopens = 0;
1984 for min_buckets in 1..=20 {
1985 let context =
1986 format!("{evaluator:?}, {direction:?}, min_buckets {min_buckets}");
1987 let mut expected = Vec::new();
1991 let mut last_emission: Option<ScanCounts> = None;
1992 for (output, counts) in &unpaced.outputs {
1993 if matches!(output, Ok(Watermarked::Watermark(_)))
1994 && let Some(last) = last_emission
1995 {
1996 if counts.work(policy) - last.work(policy) < min_buckets {
1997 continue;
1998 }
1999 if counts.rows - last.rows < min_buckets {
2000 due_only_to_reopens += 1;
2001 }
2002 }
2003 last_emission = Some(*counts);
2004 expected.push(output.clone());
2005 }
2006 let paced = charged_member_run(
2007 evaluator,
2008 sparse_and_dense_source(),
2009 skewed_and_query(),
2010 range.clone(),
2011 direction,
2012 policy,
2013 charged_buckets(min_buckets, false),
2014 )
2015 .await;
2016 assert_eq!(paced.member_outputs(), expected, "{context}");
2017 }
2018 assert!(
2019 due_only_to_reopens > 0,
2020 "no watermark was due only because of reopens: {evaluator:?}, {direction:?}"
2021 );
2022 }
2023 }
2024 }
2025}