1use bytes::Bytes;
5use futures::channel::oneshot;
6use futures::future::Either;
7use futures::{FutureExt, TryFutureExt};
8use object_store::path::Path;
9use std::collections::BinaryHeap;
10use std::fmt::Debug;
11use std::future::Future;
12use std::num::NonZero;
13use std::ops::Range;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex};
16use std::time::Instant;
17use tokio::sync::Notify;
18
19use lance_core::utils::io_stats::IoStatsRecorder;
20use lance_core::utils::parse::str_is_truthy;
21use lance_core::{Error, Result};
22
23use crate::object_store::ObjectStore;
24use crate::traits::Reader;
25use crate::utils::CachedFileSize;
26
27mod lite;
28
29const BACKPRESSURE_MIN: u64 = 5;
31const BACKPRESSURE_DEBOUNCE: u64 = 60;
33const SCHEDULER_STATE_EVENT_TARGET: &str = "lance_io::scheduler::state";
34
35static IOPS_COUNTER: AtomicU64 = AtomicU64::new(0);
37static BYTES_READ_COUNTER: AtomicU64 = AtomicU64::new(0);
39
40pub fn iops_counter() -> u64 {
41 IOPS_COUNTER.load(Ordering::Acquire)
42}
43
44pub fn bytes_read_counter() -> u64 {
45 BYTES_READ_COUNTER.load(Ordering::Acquire)
46}
47
48struct PrioritiesInFlight {
57 in_flight: Vec<u128>,
58}
59
60impl PrioritiesInFlight {
61 fn new(capacity: u32) -> Self {
62 Self {
63 in_flight: Vec::with_capacity(capacity as usize * 2),
64 }
65 }
66
67 fn min_in_flight(&self) -> u128 {
68 self.in_flight.first().copied().unwrap_or(u128::MAX)
69 }
70
71 fn contains(&self, prio: u128) -> bool {
72 self.in_flight.binary_search(&prio).is_ok()
73 }
74
75 fn push(&mut self, prio: u128) {
76 let pos = match self.in_flight.binary_search(&prio) {
77 Ok(pos) => pos,
78 Err(pos) => pos,
79 };
80 self.in_flight.insert(pos, prio);
81 }
82
83 fn remove(&mut self, prio: u128) {
84 if let Ok(pos) = self.in_flight.binary_search(&prio) {
85 self.in_flight.remove(pos);
86 }
87 }
88
89 fn len(&self) -> usize {
90 self.in_flight.len()
91 }
92
93 fn is_empty(&self) -> bool {
94 self.in_flight.is_empty()
95 }
96}
97
98struct IoQueueState {
99 io_capacity: u32,
101 iops_avail: u32,
103 io_buffer_size: u64,
105 bytes_avail: i64,
109 pending_requests: BinaryHeap<IoTask>,
111 priorities_in_flight: PrioritiesInFlight,
113 done_scheduling: bool,
116 start: Instant,
118 last_warn: AtomicU64,
120 no_backpressure: bool,
122}
123
124impl IoQueueState {
125 fn new(io_capacity: u32, io_buffer_size: u64) -> Self {
126 Self {
127 io_capacity,
128 iops_avail: io_capacity,
129 io_buffer_size,
130 bytes_avail: io_buffer_size as i64,
131 pending_requests: BinaryHeap::new(),
132 priorities_in_flight: PrioritiesInFlight::new(io_capacity),
133 done_scheduling: false,
134 start: Instant::now(),
135 last_warn: AtomicU64::from(0),
136 no_backpressure: io_buffer_size == 0,
137 }
138 }
139
140 fn scheduler_state_event(&self) -> Option<SchedulerStateEvent> {
141 if !tracing::enabled!(target: SCHEDULER_STATE_EVENT_TARGET, tracing::Level::TRACE) {
142 return None;
143 }
144
145 let pending_bytes = self
146 .pending_requests
147 .iter()
148 .map(IoTask::num_bytes)
149 .sum::<u64>();
150 let head_task = self.pending_requests.peek();
151 let min_in_flight_priority = if self.priorities_in_flight.is_empty() {
152 None
153 } else {
154 Some(self.priorities_in_flight.min_in_flight())
155 };
156 let head_task_priority_bypass = head_task.map(|task| {
157 self.no_backpressure
158 || task.bypass_backpressure
159 || task.priority <= self.priorities_in_flight.min_in_flight()
160 });
161 let head_task_blocked_by_iops = head_task.map(|_| self.iops_avail == 0);
162 let head_task_blocked_by_bytes = head_task.map(|task| {
163 let bypasses_bytes = self.no_backpressure
164 || task.bypass_backpressure
165 || task.priority <= self.priorities_in_flight.min_in_flight();
166 !bypasses_bytes && task.num_bytes() as i64 > self.bytes_avail
167 });
168 let head_task_can_deliver = head_task.map(|task| self.can_deliver_without_warning(task));
169 let head_task_bytes = head_task.map(IoTask::num_bytes);
170 let (head_task_priority_high, head_task_priority_low) =
171 split_priority(head_task.map(|task| task.priority));
172 let (min_in_flight_priority_high, min_in_flight_priority_low) =
173 split_priority(min_in_flight_priority);
174
175 Some(SchedulerStateEvent {
176 queue_kind: "standard",
177 io_capacity: u64::from(self.io_capacity),
178 iops_available: u64::from(self.iops_avail),
179 active_iops: u64::from(self.io_capacity.saturating_sub(self.iops_avail)),
180 pending_iops: self.pending_requests.len() as u64,
181 pending_bytes,
182 bytes_available: self.bytes_avail,
183 bytes_reserved: self.io_buffer_size as i64 - self.bytes_avail,
184 io_buffer_size_bytes: self.io_buffer_size,
185 priorities_in_flight: self.priorities_in_flight.len() as u64,
186 no_backpressure: self.no_backpressure,
187 head_task_bytes,
188 head_task_priority_high,
189 head_task_priority_low,
190 min_in_flight_priority_high,
191 min_in_flight_priority_low,
192 head_task_can_deliver,
193 head_task_priority_bypass,
194 head_task_blocked_by_iops,
195 head_task_blocked_by_bytes,
196 })
197 }
198
199 fn warn_if_needed(&self) {
200 let seconds_elapsed = self.start.elapsed().as_secs();
201 let last_warn = self.last_warn.load(Ordering::Acquire);
202 let since_last_warn = seconds_elapsed - last_warn;
203 if (last_warn == 0
204 && seconds_elapsed > BACKPRESSURE_MIN
205 && seconds_elapsed < BACKPRESSURE_DEBOUNCE)
206 || since_last_warn > BACKPRESSURE_DEBOUNCE
207 {
208 tracing::event!(tracing::Level::DEBUG, "Backpressure throttle exceeded");
209 log::debug!(
210 "Backpressure throttle is full, I/O will pause until buffer is drained. Max I/O bandwidth will not be achieved because CPU is falling behind"
211 );
212 self.last_warn
213 .store(seconds_elapsed.max(1), Ordering::Release);
214 }
215 }
216
217 fn can_deliver(&self, task: &IoTask) -> bool {
218 let can_deliver = self.can_deliver_without_warning(task);
219 if !can_deliver
220 && self.iops_avail > 0
221 && !(self.no_backpressure
222 || task.bypass_backpressure
223 || task.priority <= self.priorities_in_flight.min_in_flight())
224 && task.num_bytes() as i64 > self.bytes_avail
225 {
226 self.warn_if_needed();
227 }
228 can_deliver
229 }
230
231 fn can_deliver_without_warning(&self, task: &IoTask) -> bool {
232 if self.iops_avail == 0 {
233 false
234 } else if self.no_backpressure
235 || task.bypass_backpressure
236 || task.priority <= self.priorities_in_flight.min_in_flight()
237 || self.priorities_in_flight.contains(task.priority)
241 {
242 true
243 } else {
244 task.num_bytes() as i64 <= self.bytes_avail
245 }
246 }
247
248 fn next_task(&mut self) -> Option<IoTask> {
249 let task = self.pending_requests.peek()?;
250 if self.can_deliver(task) {
251 let skip_bytes_accounting = self.no_backpressure || task.bypass_backpressure;
252 self.priorities_in_flight.push(task.priority);
253 self.iops_avail -= 1;
254 if !skip_bytes_accounting {
255 self.bytes_avail -= task.num_bytes() as i64;
256 if self.bytes_avail < 0 {
257 log::debug!(
259 "Backpressure throttle temporarily exceeded by {} bytes due to priority I/O",
260 -self.bytes_avail
261 );
262 }
263 }
264 Some(self.pending_requests.pop().unwrap())
265 } else {
266 None
267 }
268 }
269}
270
271struct IoQueue {
276 state: Mutex<IoQueueState>,
278 notify: Notify,
280 stats: IoStats,
281}
282
283impl IoQueue {
284 fn new(io_capacity: u32, io_buffer_size: u64, stats: IoStats) -> Self {
285 Self {
286 state: Mutex::new(IoQueueState::new(io_capacity, io_buffer_size)),
287 notify: Notify::new(),
288 stats,
289 }
290 }
291
292 fn push(&self, task: IoTask) {
293 log::trace!(
294 "Inserting I/O request for {} bytes with priority ({},{}) into I/O queue",
295 task.num_bytes(),
296 task.priority >> 64,
297 task.priority & 0xFFFFFFFFFFFFFFFF
298 );
299 let event = {
300 let mut state = self.state.lock().unwrap();
301 state.pending_requests.push(task);
302 state.scheduler_state_event()
303 };
304 emit_scheduler_state_event(event, &self.stats);
305
306 self.notify.notify_one();
307 }
308
309 async fn pop(&self) -> Option<IoTask> {
310 loop {
311 {
312 let mut state = self.state.lock().unwrap();
313 if let Some(task) = state.next_task() {
314 let event = state.scheduler_state_event();
315 drop(state);
316 emit_scheduler_state_event(event, &self.stats);
317 return Some(task);
318 }
319
320 if state.done_scheduling {
321 return None;
322 }
323 }
324
325 self.notify.notified().await;
326 }
327 }
328
329 fn on_iop_complete(&self) {
330 let event = {
331 let mut state = self.state.lock().unwrap();
332 state.iops_avail += 1;
333 state.scheduler_state_event()
334 };
335 emit_scheduler_state_event(event, &self.stats);
336
337 self.notify.notify_one();
338 }
339
340 fn on_bytes_consumed(&self, bytes: u64, priority: u128, num_reqs: usize) {
341 let event = {
342 let mut state = self.state.lock().unwrap();
343 state.bytes_avail += bytes as i64;
344 for _ in 0..num_reqs {
345 state.priorities_in_flight.remove(priority);
346 }
347 state.scheduler_state_event()
348 };
349 emit_scheduler_state_event(event, &self.stats);
350
351 self.notify.notify_one();
352 }
353
354 fn close(&self) {
355 let (pending_requests, event) = {
356 let mut state = self.state.lock().unwrap();
357 state.done_scheduling = true;
358 let pending_requests = std::mem::take(&mut state.pending_requests);
359 let event = state.scheduler_state_event();
360 (pending_requests, event)
361 };
362 emit_scheduler_state_event(event, &self.stats);
363 for request in pending_requests {
364 request.cancel();
365 }
366
367 self.notify.notify_one();
368 }
369}
370
371struct MutableBatch<F: FnOnce(Response) + Send> {
376 when_done: Option<F>,
377 data_buffers: Vec<Bytes>,
378 num_bytes: u64,
379 priority: u128,
380 num_reqs: usize,
381 num_delivered: usize,
382 err: Option<Error>,
383 bypass_backpressure: bool,
385 io_queue: Arc<IoQueue>,
388}
389
390impl<F: FnOnce(Response) + Send> MutableBatch<F> {
391 fn new(
392 when_done: F,
393 num_data_buffers: u32,
394 priority: u128,
395 num_reqs: usize,
396 bypass_backpressure: bool,
397 io_queue: Arc<IoQueue>,
398 ) -> Self {
399 Self {
400 when_done: Some(when_done),
401 data_buffers: vec![Bytes::default(); num_data_buffers as usize],
402 num_bytes: 0,
403 priority,
404 num_reqs,
405 num_delivered: 0,
406 err: None,
407 bypass_backpressure,
408 io_queue,
409 }
410 }
411}
412
413impl<F: FnOnce(Response) + Send> Drop for MutableBatch<F> {
418 fn drop(&mut self) {
419 let result = if let Some(err) = self.err.take() {
421 Err(err)
422 } else if self.num_delivered < self.data_buffers.len() {
423 Err(Error::io(format!(
425 "I/O request was dropped before completion ({} of {} reads delivered)",
426 self.num_delivered,
427 self.data_buffers.len()
428 )))
429 } else {
430 let mut data = Vec::new();
431 std::mem::swap(&mut data, &mut self.data_buffers);
432 Ok(data)
433 };
434 let response = Response {
437 data: Some(result),
438 io_queue: self.io_queue.clone(),
439 num_bytes: if self.bypass_backpressure {
441 0
442 } else {
443 self.num_bytes
444 },
445 priority: self.priority,
446 num_reqs: self.num_reqs,
447 };
448 (self.when_done.take().unwrap())(response);
449 }
450}
451
452struct DataChunk {
453 task_idx: usize,
454 num_bytes: u64,
455 data: Result<Bytes>,
456}
457
458trait DataSink: Send {
459 fn deliver_data(&mut self, data: DataChunk);
460}
461
462impl<F: FnOnce(Response) + Send> DataSink for MutableBatch<F> {
463 fn deliver_data(&mut self, data: DataChunk) {
465 self.num_bytes += data.num_bytes;
466 self.num_delivered += 1;
467 match data.data {
468 Ok(data_bytes) => {
469 self.data_buffers[data.task_idx] = data_bytes;
470 }
471 Err(err) => {
472 self.err.get_or_insert(err);
474 }
475 }
476 }
477}
478
479struct IoTask {
480 reader: Arc<dyn Reader>,
481 to_read: Range<u64>,
482 when_done: Box<dyn FnOnce(Result<Bytes>) + Send>,
483 priority: u128,
484 bypass_backpressure: bool,
485}
486
487fn validate_read_length(
488 file_path: &Path,
489 requested_range: &Range<u64>,
490 bytes: Bytes,
491) -> Result<Bytes> {
492 let expected_len = requested_range.end - requested_range.start;
493 if bytes.len() as u64 != expected_len {
494 return Err(Error::io(format!(
495 "I/O request for file {file_path} and range {}..{} returned {} bytes, expected {expected_len} bytes",
496 requested_range.start,
497 requested_range.end,
498 bytes.len()
499 )));
500 }
501 Ok(bytes)
502}
503
504impl Eq for IoTask {}
505
506impl PartialEq for IoTask {
507 fn eq(&self, other: &Self) -> bool {
508 self.bypass_backpressure == other.bypass_backpressure && self.priority == other.priority
509 }
510}
511
512impl PartialOrd for IoTask {
513 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
514 Some(self.cmp(other))
515 }
516}
517
518impl Ord for IoTask {
519 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
520 self.bypass_backpressure
523 .cmp(&other.bypass_backpressure)
524 .then(other.priority.cmp(&self.priority))
525 }
526}
527
528impl IoTask {
529 fn num_bytes(&self) -> u64 {
530 self.to_read.end - self.to_read.start
531 }
532 fn cancel(self) {
533 (self.when_done)(Err(Error::internal(
534 "Scheduler closed before I/O was completed".to_string(),
535 )));
536 }
537
538 async fn run(self) {
539 let file_path = self.reader.path().as_ref();
540 let num_bytes = self.num_bytes();
541 let bytes = if self.to_read.start == self.to_read.end {
542 Ok(Bytes::new())
543 } else {
544 let bytes_fut = self
545 .reader
546 .get_range(self.to_read.start as usize..self.to_read.end as usize);
547 IOPS_COUNTER.fetch_add(1, Ordering::Release);
548 let num_bytes = self.num_bytes();
549 bytes_fut
550 .inspect(move |_| {
551 BYTES_READ_COUNTER.fetch_add(num_bytes, Ordering::Release);
552 })
553 .await
554 .map_err(Error::from)
555 .and_then(|bytes| validate_read_length(self.reader.path(), &self.to_read, bytes))
556 };
557 tracing::trace!(
559 file = file_path,
560 bytes_read = num_bytes,
561 requests = 1,
562 range_start = self.to_read.start,
563 range_end = self.to_read.end,
564 "File I/O completed"
565 );
566 (self.when_done)(bytes);
567 }
568}
569
570async fn run_io_loop(tasks: Arc<IoQueue>) {
573 loop {
576 let next_task = tasks.pop().await;
577 match next_task {
578 Some(task) => {
579 tokio::spawn(task.run());
580 }
581 None => {
582 return;
584 }
585 }
586 }
587}
588
589#[derive(Debug)]
590struct StatsCollector {
591 iops: AtomicU64,
592 requests: AtomicU64,
593 bytes_read: AtomicU64,
594}
595
596impl StatsCollector {
597 fn new() -> Self {
598 Self {
599 iops: AtomicU64::new(0),
600 requests: AtomicU64::new(0),
601 bytes_read: AtomicU64::new(0),
602 }
603 }
604
605 fn iops(&self) -> u64 {
606 self.iops.load(Ordering::Relaxed)
607 }
608
609 fn bytes_read(&self) -> u64 {
610 self.bytes_read.load(Ordering::Relaxed)
611 }
612
613 fn requests(&self) -> u64 {
614 self.requests.load(Ordering::Relaxed)
615 }
616
617 fn record_request(&self, request: &[Range<u64>]) {
618 self.requests.fetch_add(1, Ordering::Relaxed);
619 self.iops.fetch_add(request.len() as u64, Ordering::Relaxed);
620 self.bytes_read.fetch_add(
621 request.iter().map(|r| r.end - r.start).sum::<u64>(),
622 Ordering::Relaxed,
623 );
624 }
625
626 fn add(&self, iops: u64, requests: u64, bytes_read: u64) {
629 self.iops.fetch_add(iops, Ordering::Relaxed);
630 self.requests.fetch_add(requests, Ordering::Relaxed);
631 self.bytes_read.fetch_add(bytes_read, Ordering::Relaxed);
632 }
633}
634
635impl IoStatsRecorder for StatsCollector {
636 fn record_request(&self, request: &[Range<u64>]) {
637 Self::record_request(self, request)
640 }
641}
642
643#[derive(Debug, Clone, Copy, Default)]
644pub struct ScanStats {
645 pub iops: u64,
646 pub requests: u64,
647 pub bytes_read: u64,
648}
649
650impl ScanStats {
651 fn new(stats: &StatsCollector) -> Self {
652 Self {
653 iops: stats.iops(),
654 requests: stats.requests(),
655 bytes_read: stats.bytes_read(),
656 }
657 }
658}
659
660fn split_priority(priority: Option<u128>) -> (Option<u64>, Option<u64>) {
661 priority
662 .map(|priority| ((priority >> 64) as u64, priority as u64))
663 .unzip()
664}
665
666#[derive(Debug, Clone, Copy)]
667pub(super) struct SchedulerStateEvent {
668 pub(super) queue_kind: &'static str,
669 pub(super) io_capacity: u64,
670 pub(super) iops_available: u64,
671 pub(super) active_iops: u64,
672 pub(super) pending_iops: u64,
673 pub(super) pending_bytes: u64,
674 pub(super) bytes_available: i64,
675 pub(super) bytes_reserved: i64,
676 pub(super) io_buffer_size_bytes: u64,
677 pub(super) priorities_in_flight: u64,
678 pub(super) no_backpressure: bool,
679 pub(super) head_task_bytes: Option<u64>,
680 pub(super) head_task_priority_high: Option<u64>,
681 pub(super) head_task_priority_low: Option<u64>,
682 pub(super) min_in_flight_priority_high: Option<u64>,
683 pub(super) min_in_flight_priority_low: Option<u64>,
684 pub(super) head_task_can_deliver: Option<bool>,
685 pub(super) head_task_priority_bypass: Option<bool>,
686 pub(super) head_task_blocked_by_iops: Option<bool>,
687 pub(super) head_task_blocked_by_bytes: Option<bool>,
688}
689
690impl SchedulerStateEvent {
691 fn trace(self, stats: ScanStats) {
692 tracing::event!(
693 target: SCHEDULER_STATE_EVENT_TARGET,
694 tracing::Level::TRACE,
695 queue_kind = self.queue_kind,
696 scheduler_iops = stats.iops,
697 scheduler_requests = stats.requests,
698 scheduler_bytes_read = stats.bytes_read,
699 io_capacity = self.io_capacity,
700 iops_available = self.iops_available,
701 active_iops = self.active_iops,
702 pending_iops = self.pending_iops,
703 pending_bytes = self.pending_bytes,
704 bytes_available = self.bytes_available,
705 bytes_reserved = self.bytes_reserved,
706 io_buffer_size_bytes = self.io_buffer_size_bytes,
707 priorities_in_flight = self.priorities_in_flight,
708 no_backpressure = self.no_backpressure,
709 head_task_bytes_present = self.head_task_bytes.is_some(),
710 head_task_bytes = self.head_task_bytes.unwrap_or_default(),
711 head_task_priority_high_present = self.head_task_priority_high.is_some(),
712 head_task_priority_high = self.head_task_priority_high.unwrap_or_default(),
713 head_task_priority_low_present = self.head_task_priority_low.is_some(),
714 head_task_priority_low = self.head_task_priority_low.unwrap_or_default(),
715 min_in_flight_priority_high_present = self.min_in_flight_priority_high.is_some(),
716 min_in_flight_priority_high = self.min_in_flight_priority_high.unwrap_or_default(),
717 min_in_flight_priority_low_present = self.min_in_flight_priority_low.is_some(),
718 min_in_flight_priority_low = self.min_in_flight_priority_low.unwrap_or_default(),
719 head_task_can_deliver_present = self.head_task_can_deliver.is_some(),
720 head_task_can_deliver = self.head_task_can_deliver.unwrap_or(false),
721 head_task_priority_bypass_present = self.head_task_priority_bypass.is_some(),
722 head_task_priority_bypass = self.head_task_priority_bypass.unwrap_or(false),
723 head_task_blocked_by_iops_present = self.head_task_blocked_by_iops.is_some(),
724 head_task_blocked_by_iops = self.head_task_blocked_by_iops.unwrap_or(false),
725 head_task_blocked_by_bytes_present = self.head_task_blocked_by_bytes.is_some(),
726 head_task_blocked_by_bytes = self.head_task_blocked_by_bytes.unwrap_or(false),
727 "Scheduler state"
728 );
729 }
730}
731
732pub(super) fn emit_scheduler_state_event(event: Option<SchedulerStateEvent>, stats: &IoStats) {
733 if let Some(event) = event {
734 event.trace(stats.snapshot());
735 }
736}
737
738#[derive(Debug, Clone)]
749pub struct IoStats(Arc<StatsCollector>);
750
751impl IoStats {
752 pub fn new() -> Self {
753 Self(Arc::new(StatsCollector::new()))
754 }
755
756 pub fn record_request(&self, request: &[Range<u64>]) {
760 self.0.record_request(request);
761 }
762
763 pub fn snapshot(&self) -> ScanStats {
765 ScanStats::new(self.0.as_ref())
766 }
767
768 pub fn recorder(&self) -> Arc<dyn IoStatsRecorder> {
772 self.0.clone()
773 }
774
775 pub fn add_scan_stats(&self, stats: &ScanStats) {
779 self.0.add(stats.iops, stats.requests, stats.bytes_read);
780 }
781}
782
783impl Default for IoStats {
784 fn default() -> Self {
785 Self::new()
786 }
787}
788
789enum IoQueueType {
790 Standard(Arc<IoQueue>),
791 Lite(Arc<lite::IoQueue>),
792}
793
794pub struct ScanScheduler {
803 object_store: Arc<ObjectStore>,
804 io_queue: IoQueueType,
805 stats: IoStats,
806}
807
808impl Debug for ScanScheduler {
809 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
810 f.debug_struct("ScanScheduler")
811 .field("object_store", &self.object_store)
812 .finish()
813 }
814}
815
816struct Response {
817 data: Option<Result<Vec<Bytes>>>,
820 io_queue: Arc<IoQueue>,
821 priority: u128,
822 num_reqs: usize,
823 num_bytes: u64,
824}
825
826impl Drop for Response {
830 fn drop(&mut self) {
831 self.io_queue
832 .on_bytes_consumed(self.num_bytes, self.priority, self.num_reqs);
833 }
834}
835
836#[derive(Debug, Clone, Copy)]
837pub struct SchedulerConfig {
838 pub io_buffer_size_bytes: u64,
842 pub use_lite_scheduler: Option<bool>,
848}
849
850impl SchedulerConfig {
851 pub fn new(io_buffer_size_bytes: u64) -> Self {
852 Self {
853 io_buffer_size_bytes,
854 use_lite_scheduler: std::env::var("LANCE_USE_LITE_SCHEDULER")
855 .ok()
856 .map(|v| str_is_truthy(v.trim())),
857 }
858 }
859
860 pub fn default_for_testing() -> Self {
862 Self {
863 io_buffer_size_bytes: 256 * 1024 * 1024,
864 use_lite_scheduler: None,
865 }
866 }
867
868 pub fn max_bandwidth(store: &ObjectStore) -> Self {
871 Self::new(32 * 1024 * 1024 * store.io_parallelism() as u64)
872 }
873
874 pub fn with_lite_scheduler(self) -> Self {
875 Self {
876 use_lite_scheduler: Some(true),
877 ..self
878 }
879 }
880}
881
882impl ScanScheduler {
883 pub fn new(object_store: Arc<ObjectStore>, config: SchedulerConfig) -> Arc<Self> {
890 let io_capacity = object_store.io_parallelism();
891 let stats = IoStats::new();
892 let use_lite = config
893 .use_lite_scheduler
894 .unwrap_or_else(|| object_store.prefers_lite_scheduler());
895 let io_queue = if use_lite {
896 let io_queue = Arc::new(lite::IoQueue::new(
897 io_capacity as u64,
898 config.io_buffer_size_bytes,
899 stats.clone(),
900 ));
901 IoQueueType::Lite(io_queue)
902 } else {
903 let io_queue = Arc::new(IoQueue::new(
904 io_capacity as u32,
905 config.io_buffer_size_bytes,
906 stats.clone(),
907 ));
908 let io_queue_clone = io_queue.clone();
909 tokio::task::spawn(async move { run_io_loop(io_queue_clone).await });
913 IoQueueType::Standard(io_queue)
914 };
915 Arc::new(Self {
916 object_store,
917 io_queue,
918 stats,
919 })
920 }
921
922 pub async fn open_file_with_priority(
931 self: &Arc<Self>,
932 path: &Path,
933 base_priority: u64,
934 file_size_bytes: &CachedFileSize,
935 ) -> Result<FileScheduler> {
936 let file_size_bytes = if let Some(size) = file_size_bytes.get() {
937 u64::from(size)
938 } else {
939 let size = self.object_store.size(path).await?;
940 if let Some(size) = NonZero::new(size) {
941 file_size_bytes.set(size);
942 }
943 size
944 };
945 let reader = self
946 .object_store
947 .open_with_size(path, file_size_bytes as usize)
948 .await?;
949 let block_size = self.object_store.block_size() as u64;
950 let max_iop_size = self.object_store.max_iop_size();
951 Ok(FileScheduler {
952 reader: reader.into(),
953 block_size,
954 root: self.clone(),
955 base_priority,
956 max_iop_size,
957 bypass_backpressure: false,
958 extra_stats: None,
959 })
960 }
961
962 pub async fn open_file(
966 self: &Arc<Self>,
967 path: &Path,
968 file_size_bytes: &CachedFileSize,
969 ) -> Result<FileScheduler> {
970 self.open_file_with_priority(path, 0, file_size_bytes).await
971 }
972
973 pub fn open_reader(self: &Arc<Self>, reader: Arc<dyn Reader>) -> FileScheduler {
984 FileScheduler {
985 reader,
986 block_size: self.object_store.block_size() as u64,
987 root: self.clone(),
988 base_priority: 0,
989 max_iop_size: self.object_store.max_iop_size(),
990 bypass_backpressure: false,
991 extra_stats: None,
992 }
993 }
994
995 fn do_submit_request(
996 &self,
997 reader: Arc<dyn Reader>,
998 request: Vec<Range<u64>>,
999 tx: oneshot::Sender<Response>,
1000 priority: u128,
1001 io_queue: &Arc<IoQueue>,
1002 bypass_backpressure: bool,
1003 ) {
1004 let num_iops = request.len() as u32;
1005
1006 let when_all_io_done = move |bytes_and_permits| {
1007 let _ = tx.send(bytes_and_permits);
1009 };
1010
1011 let dest = Arc::new(Mutex::new(Box::new(MutableBatch::new(
1012 when_all_io_done,
1013 num_iops,
1014 priority,
1015 request.len(),
1016 bypass_backpressure,
1017 io_queue.clone(),
1018 ))));
1019
1020 for (task_idx, iop) in request.into_iter().enumerate() {
1021 let dest = dest.clone();
1022 let io_queue_clone = io_queue.clone();
1023 let num_bytes = iop.end - iop.start;
1024 let task = IoTask {
1025 reader: reader.clone(),
1026 to_read: iop,
1027 priority,
1028 bypass_backpressure,
1029 when_done: Box::new(move |data| {
1030 io_queue_clone.on_iop_complete();
1031 let mut dest = dest.lock().unwrap();
1032 let chunk = DataChunk {
1033 data,
1034 task_idx,
1035 num_bytes,
1036 };
1037 dest.deliver_data(chunk);
1038 }),
1039 };
1040 io_queue.push(task);
1041 }
1042 }
1043
1044 fn submit_request_standard(
1045 &self,
1046 reader: Arc<dyn Reader>,
1047 request: Vec<Range<u64>>,
1048 priority: u128,
1049 io_queue: &Arc<IoQueue>,
1050 bypass_backpressure: bool,
1051 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1052 let (tx, rx) = oneshot::channel::<Response>();
1053
1054 self.do_submit_request(reader, request, tx, priority, io_queue, bypass_backpressure);
1055
1056 rx.map(|wrapped_rsp| {
1057 let mut rsp = wrapped_rsp.unwrap();
1060 rsp.data.take().unwrap()
1061 })
1062 }
1063
1064 fn submit_request_lite(
1065 &self,
1066 reader: Arc<dyn Reader>,
1067 request: Vec<Range<u64>>,
1068 priority: u128,
1069 io_queue: &Arc<lite::IoQueue>,
1070 bypass_backpressure: bool,
1071 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1072 let maybe_tasks = request
1074 .into_iter()
1075 .map(|task| {
1076 let reader = reader.clone();
1077 let queue = io_queue.clone();
1078 let requested_range = task.clone();
1079 let run_fn = Box::new(move || {
1080 let bytes_fut = reader
1081 .get_range(requested_range.start as usize..requested_range.end as usize);
1082 async move {
1083 let bytes = bytes_fut.await.map_err(Error::from)?;
1084 validate_read_length(reader.path(), &requested_range, bytes)
1085 }
1086 .boxed()
1087 });
1088 queue.submit(task, priority, run_fn, bypass_backpressure)
1089 })
1090 .collect::<Result<Vec<_>>>();
1091 match maybe_tasks {
1092 Ok(tasks) => async move {
1093 let mut results = Vec::with_capacity(tasks.len());
1094 for task in tasks {
1095 results.push(task.await?);
1096 }
1097 Ok(results)
1098 }
1099 .boxed(),
1100 Err(e) => async move { Err(e) }.boxed(),
1101 }
1102 }
1103
1104 pub fn submit_request(
1105 &self,
1106 reader: Arc<dyn Reader>,
1107 request: Vec<Range<u64>>,
1108 priority: u128,
1109 bypass_backpressure: bool,
1110 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1111 match &self.io_queue {
1112 IoQueueType::Standard(io_queue) => {
1113 futures::future::Either::Left(self.submit_request_standard(
1114 reader,
1115 request,
1116 priority,
1117 io_queue,
1118 bypass_backpressure,
1119 ))
1120 }
1121 IoQueueType::Lite(io_queue) => futures::future::Either::Right(
1122 self.submit_request_lite(reader, request, priority, io_queue, bypass_backpressure),
1123 ),
1124 }
1125 }
1126
1127 pub fn stats(&self) -> ScanStats {
1128 self.stats.snapshot()
1129 }
1130
1131 #[cfg(test)]
1132 fn uses_lite_scheduler(&self) -> bool {
1133 matches!(self.io_queue, IoQueueType::Lite(_))
1134 }
1135}
1136
1137impl Drop for ScanScheduler {
1138 fn drop(&mut self) {
1139 match &self.io_queue {
1151 IoQueueType::Standard(io_queue) => io_queue.close(),
1152 IoQueueType::Lite(io_queue) => io_queue.close(),
1153 }
1154 }
1155}
1156
1157#[derive(Clone, Debug)]
1159pub struct FileScheduler {
1160 reader: Arc<dyn Reader>,
1161 root: Arc<ScanScheduler>,
1162 block_size: u64,
1163 base_priority: u64,
1164 max_iop_size: u64,
1165 bypass_backpressure: bool,
1166 extra_stats: Option<Arc<dyn IoStatsRecorder>>,
1170}
1171
1172fn is_close_together(range1: &Range<u64>, range2: &Range<u64>, block_size: u64) -> bool {
1173 range2.start <= (range1.end + block_size)
1175}
1176
1177fn is_overlapping(range1: &Range<u64>, range2: &Range<u64>) -> bool {
1178 range1.start < range2.end && range2.start < range1.end
1179}
1180
1181impl FileScheduler {
1182 pub fn submit_request(
1197 &self,
1198 request: Vec<Range<u64>>,
1199 priority: u64,
1200 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1201 let priority = ((self.base_priority as u128) << 64) + priority as u128;
1203
1204 if let Some((range_index, ranges)) = request
1205 .windows(2)
1206 .enumerate()
1207 .find(|(_, ranges)| ranges[0].start > ranges[1].start)
1208 {
1209 return Either::Left(std::future::ready(Err(Error::invalid_input(format!(
1210 "I/O request ranges must be sorted by start offset: range at index {range_index} is {:?}, but range at index {} is {:?}",
1211 ranges[0],
1212 range_index + 1,
1213 ranges[1]
1214 )))));
1215 }
1216
1217 let mut merged_requests = Vec::with_capacity(request.len());
1218
1219 if !request.is_empty() {
1220 let mut curr_interval = request[0].clone();
1221
1222 for req in request.iter().skip(1) {
1223 if is_close_together(&curr_interval, req, self.block_size) {
1224 curr_interval.end = curr_interval.end.max(req.end);
1225 } else {
1226 merged_requests.push(curr_interval);
1227 curr_interval = req.clone();
1228 }
1229 }
1230
1231 merged_requests.push(curr_interval);
1232 }
1233
1234 let mut updated_requests = Vec::with_capacity(merged_requests.len());
1235 for req in merged_requests {
1236 if req.is_empty() {
1237 updated_requests.push(req);
1238 } else {
1239 let num_requests = (req.end - req.start).div_ceil(self.max_iop_size);
1240 let bytes_per_request = (req.end - req.start) / num_requests;
1241 for i in 0..num_requests {
1242 let start = req.start + i * bytes_per_request;
1243 let end = if i == num_requests - 1 {
1244 req.end
1246 } else {
1247 start + bytes_per_request
1248 };
1249 updated_requests.push(start..end);
1250 }
1251 }
1252 }
1253
1254 self.root.stats.record_request(&updated_requests);
1255 if let Some(extra_stats) = &self.extra_stats {
1256 extra_stats.record_request(&updated_requests);
1257 }
1258
1259 let bytes_vec_fut = self.root.submit_request(
1260 self.reader.clone(),
1261 updated_requests.clone(),
1262 priority,
1263 self.bypass_backpressure,
1264 );
1265
1266 let mut updated_index = 0;
1267 let mut final_bytes = Vec::with_capacity(request.len());
1268
1269 Either::Right(async move {
1270 let bytes_vec = bytes_vec_fut.await?;
1271
1272 let mut orig_index = 0;
1273 while (updated_index < updated_requests.len()) && (orig_index < request.len()) {
1274 let updated_range = &updated_requests[updated_index];
1275 let orig_range = &request[orig_index];
1276 let byte_offset = updated_range.start as usize;
1277
1278 if is_overlapping(updated_range, orig_range) {
1279 let start = orig_range.start as usize - byte_offset;
1281 if orig_range.end <= updated_range.end {
1282 let end = orig_range.end as usize - byte_offset;
1285 final_bytes.push(bytes_vec[updated_index].slice(start..end));
1286 } else {
1287 let orig_size = orig_range.end - orig_range.start;
1290 let mut merged_bytes = Vec::with_capacity(orig_size as usize);
1291 merged_bytes.extend_from_slice(&bytes_vec[updated_index].slice(start..));
1292 let mut copy_offset = merged_bytes.len() as u64;
1293 while copy_offset < orig_size {
1294 updated_index += 1;
1295 let next_range = &updated_requests[updated_index];
1296 let bytes_to_take =
1297 (orig_size - copy_offset).min(next_range.end - next_range.start);
1298 merged_bytes.extend_from_slice(
1299 &bytes_vec[updated_index].slice(0..bytes_to_take as usize),
1300 );
1301 copy_offset += bytes_to_take;
1302 }
1303 final_bytes.push(Bytes::from(merged_bytes));
1304 }
1305 orig_index += 1;
1306 } else {
1307 updated_index += 1;
1308 }
1309 }
1310
1311 Ok(final_bytes)
1312 })
1313 }
1314
1315 pub fn with_priority(&self, priority: u64) -> Self {
1316 Self {
1317 reader: self.reader.clone(),
1318 root: self.root.clone(),
1319 block_size: self.block_size,
1320 max_iop_size: self.max_iop_size,
1321 base_priority: priority,
1322 bypass_backpressure: self.bypass_backpressure,
1323 extra_stats: self.extra_stats.clone(),
1324 }
1325 }
1326
1327 pub fn with_io_stats(&self, stats: Arc<dyn IoStatsRecorder>) -> Self {
1337 Self {
1338 extra_stats: Some(stats),
1339 ..self.clone()
1340 }
1341 }
1342
1343 pub fn with_bypass_backpressure(&self) -> Self {
1348 Self {
1349 bypass_backpressure: true,
1350 ..self.clone()
1351 }
1352 }
1353
1354 pub fn submit_single(
1361 &self,
1362 range: Range<u64>,
1363 priority: u64,
1364 ) -> impl Future<Output = Result<Bytes>> + Send {
1365 self.submit_request(vec![range], priority)
1366 .map_ok(|vec_bytes| vec_bytes.into_iter().next().unwrap())
1367 }
1368
1369 pub fn reader(&self) -> &Arc<dyn Reader> {
1375 &self.reader
1376 }
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381 use std::{collections::VecDeque, time::Duration};
1382
1383 use futures::poll;
1384 use lance_core::utils::tempfile::TempObjFile;
1385 use rand::RngCore;
1386 use rstest::rstest;
1387
1388 use object_store::{GetRange, ObjectStore as OSObjectStore, ObjectStoreExt, memory::InMemory};
1389 use tokio::{runtime::Handle, time::timeout};
1390 use url::Url;
1391
1392 use crate::{
1393 object_store::{DEFAULT_DOWNLOAD_RETRY_COUNT, DEFAULT_MAX_IOP_SIZE},
1394 testing::MockObjectStore,
1395 };
1396
1397 use super::*;
1398
1399 fn make_task(priority: u128, bypass_backpressure: bool) -> IoTask {
1400 IoTask {
1401 reader: Arc::new(TrackingReader {
1402 get_range_count: Arc::new(AtomicU64::new(0)),
1403 path: Path::parse("test").unwrap(),
1404 }),
1405 to_read: 0..1,
1406 when_done: Box::new(|_| {}),
1407 priority,
1408 bypass_backpressure,
1409 }
1410 }
1411
1412 #[test]
1413 fn test_scheduler_state_event_fields() {
1414 use tracing_mock::{expect, subscriber};
1415
1416 let event = expect::event()
1417 .with_target(SCHEDULER_STATE_EVENT_TARGET)
1418 .at_level(tracing::Level::TRACE)
1419 .with_fields(
1420 expect::field("queue_kind")
1421 .with_value(&"standard")
1422 .and(expect::field("scheduler_iops").with_value(&7u64))
1423 .and(expect::field("scheduler_requests").with_value(&3u64))
1424 .and(expect::field("scheduler_bytes_read").with_value(&4096u64))
1425 .and(expect::field("io_capacity").with_value(&4u64))
1426 .and(expect::field("pending_iops").with_value(&1u64))
1427 .and(expect::field("bytes_available").with_value(&128i64))
1428 .and(expect::field("head_task_bytes_present").with_value(&true))
1429 .and(expect::field("head_task_bytes").with_value(&1u64))
1430 .and(expect::field("head_task_can_deliver_present").with_value(&true))
1431 .and(expect::field("head_task_can_deliver").with_value(&true)),
1432 );
1433 let (subscriber, handle) = subscriber::mock().event(event).run_with_handle();
1434
1435 let stats = IoStats::new();
1436 stats.add_scan_stats(&ScanStats {
1437 iops: 7,
1438 requests: 3,
1439 bytes_read: 4096,
1440 });
1441 let mut state = IoQueueState::new(4, 192);
1442 state.iops_avail = 2;
1443 state.bytes_avail = 128;
1444 state.pending_requests.push(make_task(1, false));
1445
1446 tracing::subscriber::with_default(subscriber, || {
1447 emit_scheduler_state_event(state.scheduler_state_event(), &stats);
1448 });
1449
1450 handle.assert_finished();
1451 }
1452
1453 #[test]
1454 fn test_iotask_ordering() {
1455 let mut heap = BinaryHeap::new();
1458 heap.push(make_task(10, false)); heap.push(make_task(1, false)); heap.push(make_task(20, true)); heap.push(make_task(5, true)); let order: Vec<(u128, bool)> = std::iter::from_fn(|| heap.pop())
1464 .map(|t| (t.priority, t.bypass_backpressure))
1465 .collect();
1466
1467 assert_eq!(order, vec![(5, true), (20, true), (1, false), (10, false)]);
1468 }
1469
1470 #[test]
1471 fn test_batch_with_undelivered_slot_is_error() {
1472 let response = Arc::new(Mutex::new(None));
1473 let response_clone = response.clone();
1474 let io_queue = Arc::new(IoQueue::new(1, 1024, IoStats::new()));
1475 let batch = MutableBatch::new(
1476 move |rsp| *response_clone.lock().unwrap() = Some(rsp),
1477 2, 0, 2, false,
1481 io_queue,
1482 );
1483 drop(batch);
1484
1485 let mut rsp = response.lock().unwrap().take().unwrap();
1486 let data = rsp.data.take().unwrap();
1487 assert!(
1488 data.is_err(),
1489 "undelivered slot must yield an error, got {data:?}",
1490 );
1491 }
1492
1493 #[tokio::test]
1494 async fn test_full_seq_read() {
1495 let tmp_file = TempObjFile::default();
1496
1497 let obj_store = Arc::new(ObjectStore::local());
1498
1499 const DATA_SIZE: u64 = 1024 * 1024;
1501 let mut some_data = vec![0; DATA_SIZE as usize];
1502 rand::rng().fill_bytes(&mut some_data);
1503 obj_store.put(&tmp_file, &some_data).await.unwrap();
1504
1505 let config = SchedulerConfig::default_for_testing();
1506
1507 let scheduler = ScanScheduler::new(obj_store, config);
1508
1509 let file_scheduler = scheduler
1510 .open_file(&tmp_file, &CachedFileSize::unknown())
1511 .await
1512 .unwrap();
1513
1514 const READ_SIZE: u64 = 4 * 1024;
1516 let mut reqs = VecDeque::new();
1517 let mut offset = 0;
1518 while offset < DATA_SIZE {
1519 reqs.push_back(
1520 #[allow(clippy::single_range_in_vec_init)]
1521 file_scheduler
1522 .submit_request(vec![offset..offset + READ_SIZE], 0)
1523 .await
1524 .unwrap(),
1525 );
1526 offset += READ_SIZE;
1527 }
1528
1529 offset = 0;
1530 while offset < DATA_SIZE {
1532 let data = reqs.pop_front().unwrap();
1533 let actual = &data[0];
1534 let expected = &some_data[offset as usize..(offset + READ_SIZE) as usize];
1535 assert_eq!(expected, actual);
1536 offset += READ_SIZE;
1537 }
1538 }
1539
1540 #[tokio::test]
1541 async fn test_open_reader_bridge() {
1542 let tmp_file = TempObjFile::default();
1543
1544 let obj_store = Arc::new(ObjectStore::local());
1545
1546 const DATA_SIZE: u64 = 64 * 1024;
1547 let mut some_data = vec![0; DATA_SIZE as usize];
1548 rand::rng().fill_bytes(&mut some_data);
1549 obj_store.put(&tmp_file, &some_data).await.unwrap();
1550
1551 let config = SchedulerConfig::default_for_testing();
1552 let scheduler = ScanScheduler::new(obj_store.clone(), config);
1553
1554 let reader: Arc<dyn Reader> = obj_store.open(&tmp_file).await.unwrap().into();
1556 let file_scheduler = scheduler.open_reader(reader);
1557
1558 let bytes = file_scheduler
1559 .submit_request(vec![0..DATA_SIZE], 0)
1560 .await
1561 .unwrap();
1562 assert_eq!(bytes[0], some_data);
1563 }
1564
1565 #[derive(Debug)]
1566 struct ShortReader {
1567 path: Path,
1568 }
1569
1570 impl lance_core::deepsize::DeepSizeOf for ShortReader {
1571 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
1572 0
1573 }
1574 }
1575
1576 impl Reader for ShortReader {
1577 fn path(&self) -> &Path {
1578 &self.path
1579 }
1580
1581 fn block_size(&self) -> usize {
1582 4096
1583 }
1584
1585 fn io_parallelism(&self) -> usize {
1586 1
1587 }
1588
1589 fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
1590 Box::pin(async { Ok(0) })
1591 }
1592
1593 fn get_range(
1594 &self,
1595 _range: Range<usize>,
1596 ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
1597 Box::pin(async { Ok(Bytes::new()) })
1598 }
1599
1600 fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
1601 Box::pin(async { Ok(Bytes::new()) })
1602 }
1603 }
1604
1605 #[rstest]
1606 #[case::standard(false)]
1607 #[case::lite(true)]
1608 #[tokio::test]
1609 async fn test_short_read_returns_io_error(#[case] use_lite_scheduler: bool) {
1610 let config = SchedulerConfig {
1611 use_lite_scheduler: Some(use_lite_scheduler),
1612 ..SchedulerConfig::default_for_testing()
1613 };
1614 let scheduler = ScanScheduler::new(Arc::new(ObjectStore::memory()), config);
1615 let reader = Arc::new(ShortReader {
1616 path: Path::parse("short-file").unwrap(),
1617 });
1618 let file_scheduler = scheduler.open_reader(reader);
1619
1620 let error = file_scheduler
1621 .submit_request(vec![0..8], 0)
1622 .await
1623 .unwrap_err();
1624
1625 assert!(matches!(error, Error::IO { .. }), "{error:?}");
1626 assert!(
1627 error.to_string().contains(
1628 "I/O request for file short-file and range 0..8 returned 0 bytes, expected 8 bytes"
1629 ),
1630 "{error}"
1631 );
1632 }
1633
1634 #[tokio::test]
1635 async fn test_split_coalesce() {
1636 let tmp_file = TempObjFile::default();
1637
1638 let obj_store = Arc::new(ObjectStore::local());
1639
1640 const DATA_SIZE: u64 = 75 * 1024 * 1024;
1642 let mut some_data = vec![0; DATA_SIZE as usize];
1643 rand::rng().fill_bytes(&mut some_data);
1644 obj_store.put(&tmp_file, &some_data).await.unwrap();
1645
1646 let config = SchedulerConfig::default_for_testing();
1647
1648 let scheduler = ScanScheduler::new(obj_store, config);
1649
1650 let file_scheduler = scheduler
1651 .open_file(&tmp_file, &CachedFileSize::unknown())
1652 .await
1653 .unwrap();
1654
1655 let req =
1658 file_scheduler.submit_request(vec![50_000..51_000, 52_000..53_000, 54_000..55_000], 0);
1659
1660 let bytes = req.await.unwrap();
1661
1662 assert_eq!(bytes[0], &some_data[50_000..51_000]);
1663 assert_eq!(bytes[1], &some_data[52_000..53_000]);
1664 assert_eq!(bytes[2], &some_data[54_000..55_000]);
1665
1666 assert_eq!(1, scheduler.stats().iops);
1667
1668 let req = file_scheduler.submit_request(vec![0..DATA_SIZE], 0);
1670 let bytes = req.await.unwrap();
1671 assert!(bytes[0] == some_data, "data is not the same");
1672
1673 assert_eq!(6, scheduler.stats().iops);
1674
1675 let chunk_size = *DEFAULT_MAX_IOP_SIZE;
1679 let req = file_scheduler.submit_request(
1680 vec![
1681 10..chunk_size,
1682 chunk_size + 10..(chunk_size * 2) - 20,
1683 chunk_size * 2..(chunk_size * 2) + 10,
1684 ],
1685 0,
1686 );
1687
1688 let bytes = req.await.unwrap();
1689 let chunk_size = chunk_size as usize;
1690 assert!(
1691 bytes[0] == some_data[10..chunk_size],
1692 "data is not the same"
1693 );
1694 assert!(
1695 bytes[1] == some_data[chunk_size + 10..(chunk_size * 2) - 20],
1696 "data is not the same"
1697 );
1698 assert!(
1699 bytes[2] == some_data[chunk_size * 2..(chunk_size * 2) + 10],
1700 "data is not the same"
1701 );
1702 assert_eq!(8, scheduler.stats().iops);
1703
1704 let reads = (0..44)
1705 .map(|i| i * 1_000_000..(i + 1) * 1_000_000)
1706 .collect::<Vec<_>>();
1707 let req = file_scheduler.submit_request(reads, 0);
1708 let bytes = req.await.unwrap();
1709 for (i, bytes) in bytes.iter().enumerate() {
1710 assert!(
1711 bytes == &some_data[i * 1_000_000..(i + 1) * 1_000_000],
1712 "data is not the same"
1713 );
1714 }
1715 assert_eq!(11, scheduler.stats().iops);
1716 }
1717
1718 #[rstest]
1719 #[case::standard(false)]
1720 #[case::lite(true)]
1721 #[tokio::test]
1722 async fn test_unordered_ranges_are_rejected(#[case] use_lite_scheduler: bool) {
1723 let path = Path::parse("unordered-ranges").unwrap();
1724 let source = (0_u8..64).collect::<Vec<_>>();
1725 let object_store = Arc::new(ObjectStore::memory());
1726 object_store.put(&path, &source).await.unwrap();
1727
1728 let config = SchedulerConfig {
1729 use_lite_scheduler: Some(use_lite_scheduler),
1730 ..SchedulerConfig::default_for_testing()
1731 };
1732 let scheduler = ScanScheduler::new(object_store, config);
1733 let file_scheduler = scheduler
1734 .open_file(&path, &CachedFileSize::unknown())
1735 .await
1736 .unwrap();
1737
1738 let ranges = vec![9..26, 0..49];
1739 let error = file_scheduler.submit_request(ranges, 0).await.unwrap_err();
1740
1741 assert!(matches!(error, Error::InvalidInput { .. }), "{error:?}");
1742 assert!(
1743 error.to_string().contains(
1744 "I/O request ranges must be sorted by start offset: range at index 0 is 9..26, but range at index 1 is 0..49"
1745 ),
1746 "{error}"
1747 );
1748 }
1749
1750 #[tokio::test]
1751 async fn test_io_stats_sink() {
1752 let tmp_file = TempObjFile::default();
1753 let obj_store = Arc::new(ObjectStore::local());
1754
1755 const DATA_SIZE: u64 = 1024 * 1024;
1756 let mut some_data = vec![0; DATA_SIZE as usize];
1757 rand::rng().fill_bytes(&mut some_data);
1758 obj_store.put(&tmp_file, &some_data).await.unwrap();
1759
1760 let scheduler = ScanScheduler::new(obj_store, SchedulerConfig::default_for_testing());
1761
1762 let sink = IoStats::new();
1764 let file_scheduler = scheduler
1765 .open_file(&tmp_file, &CachedFileSize::unknown())
1766 .await
1767 .unwrap()
1768 .with_io_stats(sink.recorder());
1769
1770 file_scheduler
1774 .submit_request(vec![50_000..51_000, 52_000..53_000, 54_000..55_000], 0)
1775 .await
1776 .unwrap();
1777
1778 let global = scheduler.stats();
1779 let scoped = sink.snapshot();
1780 assert_eq!(1, scoped.iops);
1781 assert_eq!(1, scoped.requests);
1782 assert_eq!(5000, scoped.bytes_read);
1784 assert_eq!(global.iops, scoped.iops);
1785 assert_eq!(global.requests, scoped.requests);
1786 assert_eq!(global.bytes_read, scoped.bytes_read);
1787
1788 let other = scheduler
1791 .open_file(&tmp_file, &CachedFileSize::unknown())
1792 .await
1793 .unwrap();
1794 other.submit_request(vec![0..1000], 0).await.unwrap();
1795
1796 let global_after = scheduler.stats();
1797 let scoped_after = sink.snapshot();
1798 assert_eq!(global.bytes_read + 1000, global_after.bytes_read);
1799 assert_eq!(scoped.bytes_read, scoped_after.bytes_read);
1800 assert_eq!(scoped.iops, scoped_after.iops);
1801 }
1802
1803 #[tokio::test]
1804 async fn test_priority() {
1805 let some_path = Path::parse("foo").unwrap();
1806 let base_store = Arc::new(InMemory::new());
1807 base_store
1808 .put(&some_path, vec![0; 1000].into())
1809 .await
1810 .unwrap();
1811
1812 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
1813 let mut obj_store = MockObjectStore::default();
1814 let semaphore_copy = semaphore.clone();
1815 obj_store
1816 .expect_get_opts()
1817 .returning(move |location, options| {
1818 let semaphore = semaphore.clone();
1819 let base_store = base_store.clone();
1820 let location = location.clone();
1821 async move {
1822 semaphore.acquire().await.unwrap().forget();
1823 base_store.get_opts(&location, options).await
1824 }
1825 .boxed()
1826 });
1827 let obj_store = Arc::new(ObjectStore::new(
1828 Arc::new(obj_store),
1829 Url::parse("mem://").unwrap(),
1830 Some(500),
1831 None,
1832 false,
1833 false,
1834 1,
1835 DEFAULT_DOWNLOAD_RETRY_COUNT,
1836 None,
1837 ));
1838
1839 let config = SchedulerConfig {
1840 io_buffer_size_bytes: 1024 * 1024,
1841 use_lite_scheduler: None,
1842 };
1843
1844 let scan_scheduler = ScanScheduler::new(obj_store, config);
1845
1846 let file_scheduler = scan_scheduler
1847 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
1848 .await
1849 .unwrap();
1850
1851 let first_fut = timeout(
1855 Duration::from_secs(10),
1856 file_scheduler.submit_single(0..10, 0),
1857 )
1858 .boxed();
1859
1860 let mut second_fut = timeout(
1862 Duration::from_secs(10),
1863 file_scheduler.submit_single(0..20, 100),
1864 )
1865 .boxed();
1866
1867 let mut third_fut = timeout(
1870 Duration::from_secs(10),
1871 file_scheduler.submit_single(0..30, 0),
1872 )
1873 .boxed();
1874
1875 semaphore_copy.add_permits(1);
1877 assert!(first_fut.await.unwrap().unwrap().len() == 10);
1878 assert!(poll!(&mut second_fut).is_pending());
1880 assert!(poll!(&mut third_fut).is_pending());
1881
1882 semaphore_copy.add_permits(1);
1884 assert!(third_fut.await.unwrap().unwrap().len() == 30);
1885 assert!(poll!(&mut second_fut).is_pending());
1886
1887 semaphore_copy.add_permits(1);
1889 assert!(second_fut.await.unwrap().unwrap().len() == 20);
1890 }
1891
1892 #[tokio::test]
1893 async fn test_standard_scheduler_state_tracks_queue_state() {
1894 let some_path = Path::parse("foo").unwrap();
1895 let base_store = Arc::new(InMemory::new());
1896 base_store
1897 .put(&some_path, vec![0; 1000].into())
1898 .await
1899 .unwrap();
1900
1901 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
1902 let mut obj_store = MockObjectStore::default();
1903 let semaphore_copy = semaphore.clone();
1904 obj_store
1905 .expect_get_opts()
1906 .returning(move |location, options| {
1907 let semaphore = semaphore.clone();
1908 let base_store = base_store.clone();
1909 let location = location.clone();
1910 async move {
1911 semaphore.acquire().await.unwrap().forget();
1912 base_store.get_opts(&location, options).await
1913 }
1914 .boxed()
1915 });
1916 let obj_store = Arc::new(ObjectStore::new(
1917 Arc::new(obj_store),
1918 Url::parse("mem://").unwrap(),
1919 Some(500),
1920 None,
1921 false,
1922 false,
1923 1,
1924 DEFAULT_DOWNLOAD_RETRY_COUNT,
1925 None,
1926 ));
1927
1928 let scheduler = ScanScheduler::new(
1929 obj_store,
1930 SchedulerConfig {
1931 io_buffer_size_bytes: 1024 * 1024,
1932 use_lite_scheduler: Some(false),
1933 },
1934 );
1935 let file_scheduler = scheduler
1936 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
1937 .await
1938 .unwrap();
1939
1940 let first_fut = timeout(
1941 Duration::from_secs(10),
1942 file_scheduler.submit_single(0..10, 0),
1943 )
1944 .boxed();
1945 let second_fut = timeout(
1946 Duration::from_secs(10),
1947 file_scheduler.submit_single(0..20, 100),
1948 )
1949 .boxed();
1950 let third_fut = timeout(
1951 Duration::from_secs(10),
1952 file_scheduler.submit_single(0..30, 0),
1953 )
1954 .boxed();
1955
1956 let io_queue = match &scheduler.io_queue {
1957 IoQueueType::Standard(io_queue) => io_queue.clone(),
1958 IoQueueType::Lite(_) => unreachable!("test forces the standard scheduler"),
1959 };
1960 let (
1961 io_capacity,
1962 iops_available,
1963 pending_bytes,
1964 bytes_reserved,
1965 priorities_in_flight,
1966 head_task_bytes,
1967 head_task_blocked_by_iops,
1968 head_task_blocked_by_bytes,
1969 ) = timeout(Duration::from_secs(5), async {
1970 loop {
1971 let observed = {
1972 let state = io_queue.state.lock().unwrap();
1973 let active_iops = state.io_capacity.saturating_sub(state.iops_avail);
1974 if active_iops == 1 && state.pending_requests.len() == 2 {
1975 let pending_bytes = state
1976 .pending_requests
1977 .iter()
1978 .map(IoTask::num_bytes)
1979 .sum::<u64>();
1980 let head_task = state.pending_requests.peek().unwrap();
1981 let bypasses_bytes = state.no_backpressure
1982 || head_task.bypass_backpressure
1983 || head_task.priority <= state.priorities_in_flight.min_in_flight();
1984 Some((
1985 state.io_capacity,
1986 state.iops_avail,
1987 pending_bytes,
1988 state.io_buffer_size as i64 - state.bytes_avail,
1989 state.priorities_in_flight.len(),
1990 head_task.num_bytes(),
1991 state.iops_avail == 0,
1992 !bypasses_bytes && head_task.num_bytes() as i64 > state.bytes_avail,
1993 ))
1994 } else {
1995 None
1996 }
1997 };
1998 if let Some(observed) = observed {
1999 break observed;
2000 }
2001 tokio::task::yield_now().await;
2002 }
2003 })
2004 .await
2005 .unwrap();
2006
2007 assert_eq!(io_capacity, 1);
2008 assert_eq!(iops_available, 0);
2009 assert_eq!(pending_bytes, 50);
2010 assert_eq!(bytes_reserved, 10);
2011 assert_eq!(priorities_in_flight, 1);
2012 assert_eq!(head_task_bytes, 30);
2013 assert!(head_task_blocked_by_iops);
2014 assert!(!head_task_blocked_by_bytes);
2015
2016 semaphore_copy.add_permits(3);
2017 assert_eq!(first_fut.await.unwrap().unwrap().len(), 10);
2018 assert_eq!(third_fut.await.unwrap().unwrap().len(), 30);
2019 assert_eq!(second_fut.await.unwrap().unwrap().len(), 20);
2020 }
2021
2022 #[tokio::test(flavor = "multi_thread")]
2023 async fn test_backpressure() {
2024 let some_path = Path::parse("foo").unwrap();
2025 let base_store = Arc::new(InMemory::new());
2026 base_store
2027 .put(&some_path, vec![0; 100000].into())
2028 .await
2029 .unwrap();
2030
2031 let bytes_read = Arc::new(AtomicU64::from(0));
2032 let mut obj_store = MockObjectStore::default();
2033 let bytes_read_copy = bytes_read.clone();
2034 obj_store
2036 .expect_get_opts()
2037 .returning(move |location, options| {
2038 let range = options.range.as_ref().unwrap();
2039 let num_bytes = match range {
2040 GetRange::Bounded(bounded) => bounded.end - bounded.start,
2041 _ => panic!(),
2042 };
2043 bytes_read_copy.fetch_add(num_bytes, Ordering::Release);
2044 let location = location.clone();
2045 let base_store = base_store.clone();
2046 async move { base_store.get_opts(&location, options).await }.boxed()
2047 });
2048 let obj_store = Arc::new(ObjectStore::new(
2049 Arc::new(obj_store),
2050 Url::parse("mem://").unwrap(),
2051 Some(500),
2052 None,
2053 false,
2054 false,
2055 1,
2056 DEFAULT_DOWNLOAD_RETRY_COUNT,
2057 None,
2058 ));
2059
2060 let config = SchedulerConfig {
2061 io_buffer_size_bytes: 10,
2062 use_lite_scheduler: None,
2063 };
2064
2065 let scan_scheduler = ScanScheduler::new(obj_store.clone(), config);
2066
2067 let file_scheduler = scan_scheduler
2068 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(100000))
2069 .await
2070 .unwrap();
2071
2072 let wait_for_idle = || async move {
2073 let handle = Handle::current();
2074 while handle.metrics().num_alive_tasks() != 1 {
2075 tokio::time::sleep(Duration::from_millis(10)).await;
2076 }
2077 };
2078 let wait_for_bytes_read_and_idle = |target_bytes: u64| {
2079 let bytes_read = &bytes_read;
2081 async move {
2082 let bytes_read_copy = bytes_read.clone();
2083 while bytes_read_copy.load(Ordering::Acquire) < target_bytes {
2084 tokio::time::sleep(Duration::from_millis(10)).await;
2085 }
2086 wait_for_idle().await;
2087 }
2088 };
2089
2090 let first_fut = file_scheduler.submit_single(0..5, 0);
2092 let second_fut = file_scheduler.submit_single(0..5, 0);
2094 let third_fut = file_scheduler.submit_single(0..3, 0);
2096 wait_for_bytes_read_and_idle(10).await;
2098
2099 assert_eq!(first_fut.await.unwrap().len(), 5);
2100 wait_for_bytes_read_and_idle(13).await;
2102
2103 let fourth_fut = file_scheduler.submit_single(0..5, 0);
2105 wait_for_bytes_read_and_idle(13).await;
2106
2107 assert_eq!(third_fut.await.unwrap().len(), 3);
2109 wait_for_bytes_read_and_idle(18).await;
2110
2111 assert_eq!(second_fut.await.unwrap().len(), 5);
2112 let fifth_fut = file_scheduler.submit_request(vec![0..3, 90000..90007], 0);
2119 wait_for_bytes_read_and_idle(21).await;
2120
2121 let fifth_bytes = tokio::time::timeout(Duration::from_secs(10), fifth_fut)
2123 .await
2124 .unwrap();
2125 assert_eq!(
2126 fifth_bytes.unwrap().iter().map(|b| b.len()).sum::<usize>(),
2127 10
2128 );
2129
2130 assert_eq!(fourth_fut.await.unwrap().len(), 5);
2132 wait_for_bytes_read_and_idle(28).await;
2133
2134 let config = SchedulerConfig {
2136 io_buffer_size_bytes: 10,
2137 use_lite_scheduler: None,
2138 };
2139
2140 let scan_scheduler = ScanScheduler::new(obj_store, config);
2141 let file_scheduler = scan_scheduler
2142 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(100000))
2143 .await
2144 .unwrap();
2145
2146 let first_fut = file_scheduler.submit_single(0..10, 0);
2147 let second_fut = file_scheduler.submit_single(0..10, 0);
2148
2149 std::thread::sleep(Duration::from_millis(100));
2150 assert_eq!(first_fut.await.unwrap().len(), 10);
2151 assert_eq!(second_fut.await.unwrap().len(), 10);
2152 }
2153
2154 #[derive(Debug)]
2155 struct BlockingReader {
2156 semaphore: Arc<tokio::sync::Semaphore>,
2157 get_range_count: Arc<AtomicU64>,
2158 path: Path,
2159 }
2160
2161 impl lance_core::deepsize::DeepSizeOf for BlockingReader {
2162 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
2163 0
2164 }
2165 }
2166
2167 impl Reader for BlockingReader {
2168 fn path(&self) -> &Path {
2169 &self.path
2170 }
2171
2172 fn block_size(&self) -> usize {
2173 4096
2174 }
2175
2176 fn io_parallelism(&self) -> usize {
2177 1
2178 }
2179
2180 fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
2181 Box::pin(async { Ok(1_000_000) })
2182 }
2183
2184 fn get_range(
2185 &self,
2186 range: Range<usize>,
2187 ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
2188 self.get_range_count.fetch_add(1, Ordering::Release);
2189 let semaphore = self.semaphore.clone();
2190 let num_bytes = range.end - range.start;
2191 Box::pin(async move {
2192 semaphore.acquire().await.unwrap().forget();
2193 Ok(Bytes::from(vec![0u8; num_bytes]))
2194 })
2195 }
2196
2197 fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
2198 Box::pin(async { Ok(Bytes::from(vec![0u8; 1_000_000])) })
2199 }
2200 }
2201
2202 #[tokio::test(flavor = "multi_thread")]
2203 async fn test_same_priority_chunks_continue_after_higher_priority_request() {
2204 let obj_store = Arc::new(ObjectStore::new(
2205 Arc::new(InMemory::new()),
2206 Url::parse("mem://").unwrap(),
2207 Some(4096),
2208 None,
2209 false,
2210 false,
2211 1,
2212 DEFAULT_DOWNLOAD_RETRY_COUNT,
2213 None,
2214 ));
2215 let scheduler = ScanScheduler::new(
2216 obj_store,
2217 SchedulerConfig {
2218 io_buffer_size_bytes: 10,
2219 use_lite_scheduler: Some(false),
2220 },
2221 );
2222 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2223 let reader: Arc<dyn Reader> = Arc::new(BlockingReader {
2224 semaphore: semaphore.clone(),
2225 get_range_count: Arc::new(AtomicU64::new(0)),
2226 path: Path::parse("test").unwrap(),
2227 });
2228
2229 let low_priority =
2230 scheduler.submit_request(reader.clone(), vec![0..6, 100..106], 10, false);
2231 let high_priority = scheduler.submit_request(reader, vec![200..204], 0, false);
2232
2233 semaphore.add_permits(3);
2234 let low_priority = timeout(Duration::from_secs(5), low_priority)
2235 .await
2236 .unwrap()
2237 .unwrap();
2238 assert_eq!(
2239 low_priority.iter().map(|bytes| bytes.len()).sum::<usize>(),
2240 12
2241 );
2242
2243 let high_priority = timeout(Duration::from_secs(5), high_priority)
2244 .await
2245 .unwrap()
2246 .unwrap();
2247 assert_eq!(high_priority[0].len(), 4);
2248 }
2249
2250 #[derive(Debug)]
2252 struct TrackingReader {
2253 get_range_count: Arc<AtomicU64>,
2254 path: Path,
2255 }
2256
2257 impl lance_core::deepsize::DeepSizeOf for TrackingReader {
2258 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
2259 0
2260 }
2261 }
2262
2263 impl Reader for TrackingReader {
2264 fn path(&self) -> &Path {
2265 &self.path
2266 }
2267
2268 fn block_size(&self) -> usize {
2269 4096
2270 }
2271
2272 fn io_parallelism(&self) -> usize {
2273 1
2274 }
2275
2276 fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
2277 Box::pin(async { Ok(1_000_000) })
2278 }
2279
2280 fn get_range(
2281 &self,
2282 range: Range<usize>,
2283 ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
2284 self.get_range_count.fetch_add(1, Ordering::Release);
2285 let num_bytes = range.end - range.start;
2286 Box::pin(async move { Ok(Bytes::from(vec![0u8; num_bytes])) })
2287 }
2288
2289 fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
2290 Box::pin(async { Ok(Bytes::from(vec![0u8; 1_000_000])) })
2291 }
2292 }
2293
2294 #[tokio::test]
2295 async fn test_lite_scheduler_submits_eagerly() {
2296 let obj_store = Arc::new(ObjectStore::memory());
2297 let config = SchedulerConfig::default_for_testing().with_lite_scheduler();
2298 let scheduler = ScanScheduler::new(obj_store, config);
2299
2300 let get_range_count = Arc::new(AtomicU64::new(0));
2301 let reader: Arc<dyn Reader> = Arc::new(TrackingReader {
2302 get_range_count: get_range_count.clone(),
2303 path: Path::parse("test").unwrap(),
2304 });
2305
2306 let fut1 = scheduler.submit_request(reader.clone(), vec![0..100], 0, false);
2309 let fut2 = scheduler.submit_request(reader.clone(), vec![100..200], 10, false);
2310 let fut3 = scheduler.submit_request(reader.clone(), vec![200..300], 20, false);
2311
2312 assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2314
2315 assert_eq!(fut1.await.unwrap()[0].len(), 100);
2317 assert_eq!(fut2.await.unwrap()[0].len(), 100);
2318 assert_eq!(fut3.await.unwrap()[0].len(), 100);
2319 }
2320
2321 #[tokio::test]
2322 async fn test_object_store_selects_scheduler() {
2323 let memory_store = Arc::new(ObjectStore::memory());
2325 assert!(!memory_store.prefers_lite_scheduler());
2326 let config = SchedulerConfig {
2327 io_buffer_size_bytes: 256 * 1024 * 1024,
2328 use_lite_scheduler: None,
2329 };
2330 let scheduler = ScanScheduler::new(memory_store.clone(), config);
2331 assert!(!scheduler.uses_lite_scheduler());
2332
2333 let uring_store = Arc::new(ObjectStore::new(
2335 Arc::new(InMemory::new()),
2336 Url::parse("file+uring:///tmp").unwrap(),
2337 None,
2338 None,
2339 false,
2340 false,
2341 8,
2342 DEFAULT_DOWNLOAD_RETRY_COUNT,
2343 None,
2344 ));
2345 assert!(uring_store.prefers_lite_scheduler());
2346 let config = SchedulerConfig {
2347 io_buffer_size_bytes: 256 * 1024 * 1024,
2348 use_lite_scheduler: None,
2349 };
2350 let scheduler = ScanScheduler::new(uring_store.clone(), config);
2351 assert!(scheduler.uses_lite_scheduler());
2352
2353 let config = SchedulerConfig {
2355 io_buffer_size_bytes: 256 * 1024 * 1024,
2356 use_lite_scheduler: Some(false),
2357 };
2358 let scheduler = ScanScheduler::new(uring_store, config);
2359 assert!(!scheduler.uses_lite_scheduler());
2360
2361 let config = SchedulerConfig {
2363 io_buffer_size_bytes: 256 * 1024 * 1024,
2364 use_lite_scheduler: Some(true),
2365 };
2366 let scheduler = ScanScheduler::new(memory_store, config);
2367 assert!(scheduler.uses_lite_scheduler());
2368 }
2369
2370 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2371 async fn stress_backpressure() {
2372 let some_path = Path::parse("foo").unwrap();
2376 let obj_store = Arc::new(ObjectStore::memory());
2377 obj_store
2378 .put(&some_path, vec![0; 100000].as_slice())
2379 .await
2380 .unwrap();
2381
2382 let config = SchedulerConfig {
2384 io_buffer_size_bytes: 1,
2385 use_lite_scheduler: None,
2386 };
2387 let scan_scheduler = ScanScheduler::new(obj_store.clone(), config);
2388 let file_scheduler = scan_scheduler
2389 .open_file(&some_path, &CachedFileSize::unknown())
2390 .await
2391 .unwrap();
2392
2393 let mut futs = Vec::with_capacity(10000);
2394 for idx in 0..10000 {
2395 futs.push(file_scheduler.submit_single(idx..idx + 1, idx));
2396 }
2397
2398 for fut in futs {
2399 fut.await.unwrap();
2400 }
2401 }
2402
2403 #[tokio::test(flavor = "multi_thread")]
2404 async fn test_zero_buffer_size_no_backpressure() {
2405 let obj_store = Arc::new(ObjectStore::memory());
2408 let config = SchedulerConfig {
2409 io_buffer_size_bytes: 0,
2410 use_lite_scheduler: Some(false),
2411 };
2412 let scheduler = ScanScheduler::new(obj_store, config);
2413
2414 let get_range_count = Arc::new(AtomicU64::new(0));
2415 let reader: Arc<dyn Reader> = Arc::new(TrackingReader {
2416 get_range_count: get_range_count.clone(),
2417 path: Path::parse("test").unwrap(),
2418 });
2419
2420 let fut1 = scheduler.submit_request(reader.clone(), vec![0..1000], 0, false);
2423 let fut2 = scheduler.submit_request(reader.clone(), vec![1000..2000], 1, false);
2424 let fut3 = scheduler.submit_request(reader.clone(), vec![2000..3000], 2, false);
2425
2426 let bytes1 = timeout(Duration::from_secs(5), fut1)
2427 .await
2428 .unwrap()
2429 .unwrap();
2430 let bytes2 = timeout(Duration::from_secs(5), fut2)
2431 .await
2432 .unwrap()
2433 .unwrap();
2434 let bytes3 = timeout(Duration::from_secs(5), fut3)
2435 .await
2436 .unwrap()
2437 .unwrap();
2438 assert_eq!(bytes1[0].len(), 1000);
2439 assert_eq!(bytes2[0].len(), 1000);
2440 assert_eq!(bytes3[0].len(), 1000);
2441 assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2442 }
2443
2444 #[tokio::test(flavor = "multi_thread")]
2445 async fn test_file_scheduler_bypass_backpressure() {
2446 let some_path = Path::parse("foo").unwrap();
2449 let base_store = Arc::new(InMemory::new());
2450 base_store
2451 .put(&some_path, vec![0u8; 1000].into())
2452 .await
2453 .unwrap();
2454
2455 let bytes_dispatched = Arc::new(AtomicU64::from(0));
2456 let mut obj_store = MockObjectStore::default();
2457 let bytes_dispatched_copy = bytes_dispatched.clone();
2458 obj_store
2459 .expect_get_opts()
2460 .returning(move |location, options| {
2461 let range = options.range.as_ref().unwrap();
2462 let num_bytes = match range {
2463 GetRange::Bounded(bounded) => bounded.end - bounded.start,
2464 _ => panic!(),
2465 };
2466 bytes_dispatched_copy.fetch_add(num_bytes, Ordering::Release);
2467 let location = location.clone();
2468 let base_store = base_store.clone();
2469 async move { base_store.get_opts(&location, options).await }.boxed()
2470 });
2471 let obj_store = Arc::new(ObjectStore::new(
2472 Arc::new(obj_store),
2473 Url::parse("mem://").unwrap(),
2474 Some(500),
2475 None,
2476 false,
2477 false,
2478 1,
2479 DEFAULT_DOWNLOAD_RETRY_COUNT,
2480 None,
2481 ));
2482
2483 let config = SchedulerConfig {
2485 io_buffer_size_bytes: 10,
2486 use_lite_scheduler: Some(false),
2487 };
2488 let scan_scheduler = ScanScheduler::new(obj_store, config);
2489 let file_scheduler = scan_scheduler
2490 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
2491 .await
2492 .unwrap();
2493 let bypass_scheduler = file_scheduler.with_bypass_backpressure();
2494
2495 let blocker_fut = file_scheduler.submit_single(0..10, 0);
2497 while bytes_dispatched.load(Ordering::Acquire) < 10 {
2498 tokio::time::sleep(Duration::from_millis(1)).await;
2499 }
2500
2501 let normal_fut = file_scheduler.submit_single(0..10, 2);
2504 let bypass_fut = bypass_scheduler.submit_single(0..10, 1);
2505
2506 while bytes_dispatched.load(Ordering::Acquire) < 20 {
2508 tokio::time::sleep(Duration::from_millis(1)).await;
2509 }
2510 tokio::time::sleep(Duration::from_millis(20)).await;
2511 assert_eq!(
2512 bytes_dispatched.load(Ordering::Acquire),
2513 20,
2514 "normal read should still be blocked while budget is exhausted"
2515 );
2516
2517 timeout(Duration::from_secs(5), blocker_fut)
2519 .await
2520 .unwrap()
2521 .unwrap();
2522 timeout(Duration::from_secs(5), bypass_fut)
2523 .await
2524 .unwrap()
2525 .unwrap();
2526 timeout(Duration::from_secs(5), normal_fut)
2527 .await
2528 .unwrap()
2529 .unwrap();
2530 assert_eq!(bytes_dispatched.load(Ordering::Acquire), 30);
2531 }
2532
2533 async fn run_caller_drop_scenario(use_lite_scheduler: bool) -> (bool, Duration) {
2539 let obj_store = Arc::new(ObjectStore::new(
2540 Arc::new(InMemory::new()),
2541 Url::parse("mem://").unwrap(),
2542 Some(4096),
2543 None,
2544 false,
2545 false,
2546 1,
2547 DEFAULT_DOWNLOAD_RETRY_COUNT,
2548 None,
2549 ));
2550 let scheduler = ScanScheduler::new(
2551 obj_store,
2552 SchedulerConfig {
2553 io_buffer_size_bytes: 100,
2554 use_lite_scheduler: Some(use_lite_scheduler),
2555 },
2556 );
2557
2558 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2559 let get_range_count = Arc::new(AtomicU64::new(0));
2560 let reader: Arc<dyn Reader> = Arc::new(BlockingReader {
2561 semaphore: semaphore.clone(),
2562 get_range_count: get_range_count.clone(),
2563 path: Path::parse("test").unwrap(),
2564 });
2565
2566 let fut1 = scheduler.submit_request(reader.clone(), vec![0..50], 0, false);
2570 let handle = tokio::spawn(async move {
2571 let _ = fut1.await;
2572 });
2573
2574 while get_range_count.load(Ordering::Acquire) == 0 {
2578 tokio::time::sleep(Duration::from_millis(1)).await;
2579 }
2580
2581 handle.abort();
2583 let _ = handle.await;
2584
2585 semaphore.add_permits(1);
2588 tokio::time::sleep(Duration::from_millis(50)).await;
2591
2592 semaphore.add_permits(1);
2595 let fut2 = scheduler.submit_request(reader, vec![100..160], 1, false);
2596
2597 let start = std::time::Instant::now();
2598 let outcome = timeout(Duration::from_secs(2), fut2).await;
2599 let elapsed = start.elapsed();
2600 match outcome {
2601 Ok(res) => {
2602 assert_eq!(res.unwrap().iter().map(|b| b.len()).sum::<usize>(), 60);
2603 (true, elapsed)
2604 }
2605 Err(_) => (false, elapsed),
2606 }
2607 }
2608
2609 #[tokio::test(flavor = "multi_thread")]
2613 async fn standard_scheduler_refunds_reservation_on_caller_drop() {
2614 let (completed, elapsed) = run_caller_drop_scenario(false).await;
2615 assert!(
2616 completed,
2617 "standard scheduler deadlocked the follow-up request (elapsed {elapsed:?}); \
2618 the dropped request's reservation was not refunded"
2619 );
2620 }
2621
2622 #[tokio::test(flavor = "multi_thread")]
2625 async fn lite_scheduler_refunds_reservation_on_caller_drop() {
2626 let (completed, elapsed) = run_caller_drop_scenario(true).await;
2627 assert!(
2628 completed,
2629 "lite scheduler deadlocked the follow-up request (elapsed {elapsed:?}); \
2630 the dropped request's reservation was not refunded"
2631 );
2632 }
2633}