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
836const MAX_BANDWIDTH_BYTES_PER_IO_THREAD: u64 = 32 * 1024 * 1024;
839
840#[derive(Debug, Clone, Copy)]
841pub struct SchedulerConfig {
842 pub io_buffer_size_bytes: u64,
846 pub use_lite_scheduler: Option<bool>,
852}
853
854impl SchedulerConfig {
855 pub fn new(io_buffer_size_bytes: u64) -> Self {
856 Self {
857 io_buffer_size_bytes,
858 use_lite_scheduler: std::env::var("LANCE_USE_LITE_SCHEDULER")
859 .ok()
860 .map(|v| str_is_truthy(v.trim())),
861 }
862 }
863
864 pub fn default_for_testing() -> Self {
866 Self {
867 io_buffer_size_bytes: 256 * 1024 * 1024,
868 use_lite_scheduler: None,
869 }
870 }
871
872 pub fn max_bandwidth(store: &ObjectStore) -> Self {
875 Self::new(Self::max_bandwidth_bytes(store.io_parallelism()))
876 }
877
878 pub fn max_bandwidth_bytes(io_parallelism: usize) -> u64 {
882 MAX_BANDWIDTH_BYTES_PER_IO_THREAD * io_parallelism.max(1) as u64
883 }
884
885 pub fn with_lite_scheduler(self) -> Self {
886 Self {
887 use_lite_scheduler: Some(true),
888 ..self
889 }
890 }
891}
892
893impl ScanScheduler {
894 pub fn new(object_store: Arc<ObjectStore>, config: SchedulerConfig) -> Arc<Self> {
901 let io_capacity = object_store.io_parallelism();
902 let stats = IoStats::new();
903 let use_lite = config
904 .use_lite_scheduler
905 .unwrap_or_else(|| object_store.prefers_lite_scheduler());
906 let io_queue = if use_lite {
907 let io_queue = Arc::new(lite::IoQueue::new(
908 io_capacity as u64,
909 config.io_buffer_size_bytes,
910 stats.clone(),
911 ));
912 IoQueueType::Lite(io_queue)
913 } else {
914 let io_queue = Arc::new(IoQueue::new(
915 io_capacity as u32,
916 config.io_buffer_size_bytes,
917 stats.clone(),
918 ));
919 let io_queue_clone = io_queue.clone();
920 tokio::task::spawn(async move { run_io_loop(io_queue_clone).await });
924 IoQueueType::Standard(io_queue)
925 };
926 Arc::new(Self {
927 object_store,
928 io_queue,
929 stats,
930 })
931 }
932
933 pub async fn open_file_with_priority(
942 self: &Arc<Self>,
943 path: &Path,
944 base_priority: u64,
945 file_size_bytes: &CachedFileSize,
946 ) -> Result<FileScheduler> {
947 let file_size_bytes = if let Some(size) = file_size_bytes.get() {
948 u64::from(size)
949 } else {
950 let size = self.object_store.size(path).await?;
951 if let Some(size) = NonZero::new(size) {
952 file_size_bytes.set(size);
953 }
954 size
955 };
956 let reader = self
957 .object_store
958 .open_with_size(path, file_size_bytes as usize)
959 .await?;
960 let block_size = self.object_store.block_size() as u64;
961 let max_iop_size = self.object_store.max_iop_size();
962 Ok(FileScheduler {
963 reader: reader.into(),
964 block_size,
965 root: self.clone(),
966 base_priority,
967 max_iop_size,
968 bypass_backpressure: false,
969 extra_stats: None,
970 })
971 }
972
973 pub async fn open_file(
977 self: &Arc<Self>,
978 path: &Path,
979 file_size_bytes: &CachedFileSize,
980 ) -> Result<FileScheduler> {
981 self.open_file_with_priority(path, 0, file_size_bytes).await
982 }
983
984 pub fn open_reader(self: &Arc<Self>, reader: Arc<dyn Reader>) -> FileScheduler {
995 FileScheduler {
996 reader,
997 block_size: self.object_store.block_size() as u64,
998 root: self.clone(),
999 base_priority: 0,
1000 max_iop_size: self.object_store.max_iop_size(),
1001 bypass_backpressure: false,
1002 extra_stats: None,
1003 }
1004 }
1005
1006 fn do_submit_request(
1007 &self,
1008 reader: Arc<dyn Reader>,
1009 request: Vec<Range<u64>>,
1010 tx: oneshot::Sender<Response>,
1011 priority: u128,
1012 io_queue: &Arc<IoQueue>,
1013 bypass_backpressure: bool,
1014 ) {
1015 let num_iops = request.len() as u32;
1016
1017 let when_all_io_done = move |bytes_and_permits| {
1018 let _ = tx.send(bytes_and_permits);
1020 };
1021
1022 let dest = Arc::new(Mutex::new(Box::new(MutableBatch::new(
1023 when_all_io_done,
1024 num_iops,
1025 priority,
1026 request.len(),
1027 bypass_backpressure,
1028 io_queue.clone(),
1029 ))));
1030
1031 for (task_idx, iop) in request.into_iter().enumerate() {
1032 let dest = dest.clone();
1033 let io_queue_clone = io_queue.clone();
1034 let num_bytes = iop.end - iop.start;
1035 let task = IoTask {
1036 reader: reader.clone(),
1037 to_read: iop,
1038 priority,
1039 bypass_backpressure,
1040 when_done: Box::new(move |data| {
1041 io_queue_clone.on_iop_complete();
1042 let mut dest = dest.lock().unwrap();
1043 let chunk = DataChunk {
1044 data,
1045 task_idx,
1046 num_bytes,
1047 };
1048 dest.deliver_data(chunk);
1049 }),
1050 };
1051 io_queue.push(task);
1052 }
1053 }
1054
1055 fn submit_request_standard(
1056 &self,
1057 reader: Arc<dyn Reader>,
1058 request: Vec<Range<u64>>,
1059 priority: u128,
1060 io_queue: &Arc<IoQueue>,
1061 bypass_backpressure: bool,
1062 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1063 let (tx, rx) = oneshot::channel::<Response>();
1064
1065 self.do_submit_request(reader, request, tx, priority, io_queue, bypass_backpressure);
1066
1067 rx.map(|wrapped_rsp| {
1068 let mut rsp = wrapped_rsp.unwrap();
1071 rsp.data.take().unwrap()
1072 })
1073 }
1074
1075 fn submit_request_lite(
1076 &self,
1077 reader: Arc<dyn Reader>,
1078 request: Vec<Range<u64>>,
1079 priority: u128,
1080 io_queue: &Arc<lite::IoQueue>,
1081 bypass_backpressure: bool,
1082 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1083 let maybe_tasks = request
1085 .into_iter()
1086 .map(|task| {
1087 let reader = reader.clone();
1088 let queue = io_queue.clone();
1089 let requested_range = task.clone();
1090 let run_fn = Box::new(move || {
1091 if requested_range.start == requested_range.end {
1095 return async { Ok(Bytes::new()) }.boxed();
1096 }
1097 let num_bytes = requested_range.end - requested_range.start;
1102 IOPS_COUNTER.fetch_add(1, Ordering::Release);
1103 let bytes_fut = reader
1104 .get_range(requested_range.start as usize..requested_range.end as usize)
1105 .inspect(move |_| {
1106 BYTES_READ_COUNTER.fetch_add(num_bytes, Ordering::Release);
1107 });
1108 async move {
1109 let bytes = bytes_fut.await.map_err(Error::from)?;
1110 validate_read_length(reader.path(), &requested_range, bytes)
1111 }
1112 .boxed()
1113 });
1114 queue.submit(task, priority, run_fn, bypass_backpressure)
1115 })
1116 .collect::<Result<Vec<_>>>();
1117 match maybe_tasks {
1118 Ok(tasks) => async move {
1119 let mut results = Vec::with_capacity(tasks.len());
1120 for task in tasks {
1121 results.push(task.await?);
1122 }
1123 Ok(results)
1124 }
1125 .boxed(),
1126 Err(e) => async move { Err(e) }.boxed(),
1127 }
1128 }
1129
1130 pub fn submit_request(
1131 &self,
1132 reader: Arc<dyn Reader>,
1133 request: Vec<Range<u64>>,
1134 priority: u128,
1135 bypass_backpressure: bool,
1136 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1137 match &self.io_queue {
1138 IoQueueType::Standard(io_queue) => {
1139 futures::future::Either::Left(self.submit_request_standard(
1140 reader,
1141 request,
1142 priority,
1143 io_queue,
1144 bypass_backpressure,
1145 ))
1146 }
1147 IoQueueType::Lite(io_queue) => futures::future::Either::Right(
1148 self.submit_request_lite(reader, request, priority, io_queue, bypass_backpressure),
1149 ),
1150 }
1151 }
1152
1153 pub fn stats(&self) -> ScanStats {
1154 self.stats.snapshot()
1155 }
1156
1157 #[cfg(test)]
1158 fn uses_lite_scheduler(&self) -> bool {
1159 matches!(self.io_queue, IoQueueType::Lite(_))
1160 }
1161}
1162
1163impl Drop for ScanScheduler {
1164 fn drop(&mut self) {
1165 match &self.io_queue {
1177 IoQueueType::Standard(io_queue) => io_queue.close(),
1178 IoQueueType::Lite(io_queue) => io_queue.close(),
1179 }
1180 }
1181}
1182
1183#[derive(Clone, Debug)]
1185pub struct FileScheduler {
1186 reader: Arc<dyn Reader>,
1187 root: Arc<ScanScheduler>,
1188 block_size: u64,
1189 base_priority: u64,
1190 max_iop_size: u64,
1191 bypass_backpressure: bool,
1192 extra_stats: Option<Arc<dyn IoStatsRecorder>>,
1196}
1197
1198fn is_close_together(range1: &Range<u64>, range2: &Range<u64>, block_size: u64) -> bool {
1199 range2.start <= range1.end.saturating_add(block_size)
1202}
1203
1204fn is_overlapping(range1: &Range<u64>, range2: &Range<u64>) -> bool {
1205 range1.start < range2.end && range2.start < range1.end
1206}
1207
1208impl FileScheduler {
1209 pub fn submit_request(
1224 &self,
1225 request: Vec<Range<u64>>,
1226 priority: u64,
1227 ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1228 let priority = ((self.base_priority as u128) << 64) + priority as u128;
1230
1231 if let Some((range_index, ranges)) = request
1232 .windows(2)
1233 .enumerate()
1234 .find(|(_, ranges)| ranges[0].start > ranges[1].start)
1235 {
1236 return Either::Left(std::future::ready(Err(Error::invalid_input(format!(
1237 "I/O request ranges must be sorted by start offset: range at index {range_index} is {:?}, but range at index {} is {:?}",
1238 ranges[0],
1239 range_index + 1,
1240 ranges[1]
1241 )))));
1242 }
1243
1244 let mut merged_requests = Vec::with_capacity(request.len());
1245
1246 if !request.is_empty() {
1247 let mut curr_interval = request[0].clone();
1248
1249 for req in request.iter().skip(1) {
1250 if is_close_together(&curr_interval, req, self.block_size) {
1251 curr_interval.end = curr_interval.end.max(req.end);
1252 } else {
1253 merged_requests.push(curr_interval);
1254 curr_interval = req.clone();
1255 }
1256 }
1257
1258 merged_requests.push(curr_interval);
1259 }
1260
1261 let mut updated_requests = Vec::with_capacity(merged_requests.len());
1262 for req in merged_requests {
1263 if req.is_empty() {
1264 updated_requests.push(req);
1265 } else {
1266 let num_requests = (req.end - req.start).div_ceil(self.max_iop_size);
1267 let bytes_per_request = (req.end - req.start) / num_requests;
1268 for i in 0..num_requests {
1269 let start = req.start + i * bytes_per_request;
1270 let end = if i == num_requests - 1 {
1271 req.end
1273 } else {
1274 start + bytes_per_request
1275 };
1276 updated_requests.push(start..end);
1277 }
1278 }
1279 }
1280
1281 self.root.stats.record_request(&updated_requests);
1282 if let Some(extra_stats) = &self.extra_stats {
1283 extra_stats.record_request(&updated_requests);
1284 }
1285
1286 let bytes_vec_fut = self.root.submit_request(
1287 self.reader.clone(),
1288 updated_requests.clone(),
1289 priority,
1290 self.bypass_backpressure,
1291 );
1292
1293 let mut updated_index = 0;
1294 let mut final_bytes = Vec::with_capacity(request.len());
1295
1296 Either::Right(async move {
1297 let bytes_vec = bytes_vec_fut.await?;
1298
1299 let mut orig_index = 0;
1300 while (updated_index < updated_requests.len()) && (orig_index < request.len()) {
1301 let updated_range = &updated_requests[updated_index];
1302 let orig_range = &request[orig_index];
1303 let byte_offset = updated_range.start as usize;
1304
1305 if is_overlapping(updated_range, orig_range) {
1306 let start = orig_range.start as usize - byte_offset;
1308 if orig_range.end <= updated_range.end {
1309 let end = orig_range.end as usize - byte_offset;
1312 final_bytes.push(bytes_vec[updated_index].slice(start..end));
1313 } else {
1314 let orig_size = orig_range.end - orig_range.start;
1317 let mut merged_bytes = Vec::with_capacity(orig_size as usize);
1318 merged_bytes.extend_from_slice(&bytes_vec[updated_index].slice(start..));
1319 let mut copy_offset = merged_bytes.len() as u64;
1320 let mut copy_index = updated_index;
1323 while copy_offset < orig_size {
1324 copy_index += 1;
1325 let next_range = &updated_requests[copy_index];
1326 let bytes_to_take =
1327 (orig_size - copy_offset).min(next_range.end - next_range.start);
1328 merged_bytes.extend_from_slice(
1329 &bytes_vec[copy_index].slice(0..bytes_to_take as usize),
1330 );
1331 copy_offset += bytes_to_take;
1332 }
1333 final_bytes.push(Bytes::from(merged_bytes));
1334 }
1335 orig_index += 1;
1336 } else {
1337 updated_index += 1;
1338 }
1339 }
1340
1341 Ok(final_bytes)
1342 })
1343 }
1344
1345 pub fn with_priority(&self, priority: u64) -> Self {
1346 Self {
1347 reader: self.reader.clone(),
1348 root: self.root.clone(),
1349 block_size: self.block_size,
1350 max_iop_size: self.max_iop_size,
1351 base_priority: priority,
1352 bypass_backpressure: self.bypass_backpressure,
1353 extra_stats: self.extra_stats.clone(),
1354 }
1355 }
1356
1357 pub fn with_io_stats(&self, stats: Arc<dyn IoStatsRecorder>) -> Self {
1367 Self {
1368 extra_stats: Some(stats),
1369 ..self.clone()
1370 }
1371 }
1372
1373 pub fn with_block_size(&self, block_size: u64) -> Self {
1382 Self {
1383 block_size,
1384 ..self.clone()
1385 }
1386 }
1387
1388 pub fn with_bypass_backpressure(&self) -> Self {
1393 Self {
1394 bypass_backpressure: true,
1395 ..self.clone()
1396 }
1397 }
1398
1399 pub fn submit_single(
1406 &self,
1407 range: Range<u64>,
1408 priority: u64,
1409 ) -> impl Future<Output = Result<Bytes>> + Send {
1410 self.submit_request(vec![range], priority)
1411 .map_ok(|vec_bytes| vec_bytes.into_iter().next().unwrap())
1412 }
1413
1414 pub fn reader(&self) -> &Arc<dyn Reader> {
1420 &self.reader
1421 }
1422}
1423
1424#[cfg(test)]
1425mod tests {
1426 use std::{collections::VecDeque, time::Duration};
1427
1428 use futures::poll;
1429 use lance_core::utils::tempfile::TempObjFile;
1430 use rand::RngCore;
1431 use rstest::rstest;
1432
1433 use object_store::{GetRange, ObjectStore as OSObjectStore, ObjectStoreExt, memory::InMemory};
1434 use tokio::{runtime::Handle, time::timeout};
1435 use url::Url;
1436
1437 use crate::{
1438 object_store::{DEFAULT_DOWNLOAD_RETRY_COUNT, DEFAULT_MAX_IOP_SIZE},
1439 testing::MockObjectStore,
1440 };
1441
1442 use super::*;
1443
1444 fn make_task(priority: u128, bypass_backpressure: bool) -> IoTask {
1445 IoTask {
1446 reader: Arc::new(TrackingReader {
1447 get_range_count: Arc::new(AtomicU64::new(0)),
1448 path: Path::parse("test").unwrap(),
1449 }),
1450 to_read: 0..1,
1451 when_done: Box::new(|_| {}),
1452 priority,
1453 bypass_backpressure,
1454 }
1455 }
1456
1457 #[test]
1458 fn test_scheduler_state_event_fields() {
1459 use tracing_mock::{expect, subscriber};
1460
1461 let event = expect::event()
1462 .with_target(SCHEDULER_STATE_EVENT_TARGET)
1463 .at_level(tracing::Level::TRACE)
1464 .with_fields(
1465 expect::field("queue_kind")
1466 .with_value(&"standard")
1467 .and(expect::field("scheduler_iops").with_value(&7u64))
1468 .and(expect::field("scheduler_requests").with_value(&3u64))
1469 .and(expect::field("scheduler_bytes_read").with_value(&4096u64))
1470 .and(expect::field("io_capacity").with_value(&4u64))
1471 .and(expect::field("pending_iops").with_value(&1u64))
1472 .and(expect::field("bytes_available").with_value(&128i64))
1473 .and(expect::field("head_task_bytes_present").with_value(&true))
1474 .and(expect::field("head_task_bytes").with_value(&1u64))
1475 .and(expect::field("head_task_can_deliver_present").with_value(&true))
1476 .and(expect::field("head_task_can_deliver").with_value(&true)),
1477 );
1478 let (subscriber, handle) = subscriber::mock().event(event).run_with_handle();
1479
1480 let stats = IoStats::new();
1481 stats.add_scan_stats(&ScanStats {
1482 iops: 7,
1483 requests: 3,
1484 bytes_read: 4096,
1485 });
1486 let mut state = IoQueueState::new(4, 192);
1487 state.iops_avail = 2;
1488 state.bytes_avail = 128;
1489 state.pending_requests.push(make_task(1, false));
1490
1491 tracing::subscriber::with_default(subscriber, || {
1492 emit_scheduler_state_event(state.scheduler_state_event(), &stats);
1493 });
1494
1495 handle.assert_finished();
1496 }
1497
1498 #[test]
1499 fn test_iotask_ordering() {
1500 let mut heap = BinaryHeap::new();
1503 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())
1509 .map(|t| (t.priority, t.bypass_backpressure))
1510 .collect();
1511
1512 assert_eq!(order, vec![(5, true), (20, true), (1, false), (10, false)]);
1513 }
1514
1515 #[test]
1516 fn test_batch_with_undelivered_slot_is_error() {
1517 let response = Arc::new(Mutex::new(None));
1518 let response_clone = response.clone();
1519 let io_queue = Arc::new(IoQueue::new(1, 1024, IoStats::new()));
1520 let batch = MutableBatch::new(
1521 move |rsp| *response_clone.lock().unwrap() = Some(rsp),
1522 2, 0, 2, false,
1526 io_queue,
1527 );
1528 drop(batch);
1529
1530 let mut rsp = response.lock().unwrap().take().unwrap();
1531 let data = rsp.data.take().unwrap();
1532 assert!(
1533 data.is_err(),
1534 "undelivered slot must yield an error, got {data:?}",
1535 );
1536 }
1537
1538 #[tokio::test]
1539 async fn test_full_seq_read() {
1540 let tmp_file = TempObjFile::default();
1541
1542 let obj_store = Arc::new(ObjectStore::local());
1543
1544 const DATA_SIZE: u64 = 1024 * 1024;
1546 let mut some_data = vec![0; DATA_SIZE as usize];
1547 rand::rng().fill_bytes(&mut some_data);
1548 obj_store.put(&tmp_file, &some_data).await.unwrap();
1549
1550 let config = SchedulerConfig::default_for_testing();
1551
1552 let scheduler = ScanScheduler::new(obj_store, config);
1553
1554 let file_scheduler = scheduler
1555 .open_file(&tmp_file, &CachedFileSize::unknown())
1556 .await
1557 .unwrap();
1558
1559 const READ_SIZE: u64 = 4 * 1024;
1561 let mut reqs = VecDeque::new();
1562 let mut offset = 0;
1563 while offset < DATA_SIZE {
1564 reqs.push_back(
1565 #[allow(clippy::single_range_in_vec_init)]
1566 file_scheduler
1567 .submit_request(vec![offset..offset + READ_SIZE], 0)
1568 .await
1569 .unwrap(),
1570 );
1571 offset += READ_SIZE;
1572 }
1573
1574 offset = 0;
1575 while offset < DATA_SIZE {
1577 let data = reqs.pop_front().unwrap();
1578 let actual = &data[0];
1579 let expected = &some_data[offset as usize..(offset + READ_SIZE) as usize];
1580 assert_eq!(expected, actual);
1581 offset += READ_SIZE;
1582 }
1583 }
1584
1585 #[tokio::test]
1586 async fn test_open_reader_bridge() {
1587 let tmp_file = TempObjFile::default();
1588
1589 let obj_store = Arc::new(ObjectStore::local());
1590
1591 const DATA_SIZE: u64 = 64 * 1024;
1592 let mut some_data = vec![0; DATA_SIZE as usize];
1593 rand::rng().fill_bytes(&mut some_data);
1594 obj_store.put(&tmp_file, &some_data).await.unwrap();
1595
1596 let config = SchedulerConfig::default_for_testing();
1597 let scheduler = ScanScheduler::new(obj_store.clone(), config);
1598
1599 let reader: Arc<dyn Reader> = obj_store.open(&tmp_file).await.unwrap().into();
1601 let file_scheduler = scheduler.open_reader(reader);
1602
1603 let bytes = file_scheduler
1604 .submit_request(vec![0..DATA_SIZE], 0)
1605 .await
1606 .unwrap();
1607 assert_eq!(bytes[0], some_data);
1608 }
1609
1610 #[derive(Debug)]
1611 struct ShortReader {
1612 path: Path,
1613 }
1614
1615 impl lance_core::deepsize::DeepSizeOf for ShortReader {
1616 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
1617 0
1618 }
1619 }
1620
1621 impl Reader for ShortReader {
1622 fn path(&self) -> &Path {
1623 &self.path
1624 }
1625
1626 fn block_size(&self) -> usize {
1627 4096
1628 }
1629
1630 fn io_parallelism(&self) -> usize {
1631 1
1632 }
1633
1634 fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
1635 Box::pin(async { Ok(0) })
1636 }
1637
1638 fn get_range(
1639 &self,
1640 _range: Range<usize>,
1641 ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
1642 Box::pin(async { Ok(Bytes::new()) })
1643 }
1644
1645 fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
1646 Box::pin(async { Ok(Bytes::new()) })
1647 }
1648 }
1649
1650 #[rstest]
1651 #[case::standard(false)]
1652 #[case::lite(true)]
1653 #[tokio::test]
1654 async fn test_short_read_returns_io_error(#[case] use_lite_scheduler: bool) {
1655 let config = SchedulerConfig {
1656 use_lite_scheduler: Some(use_lite_scheduler),
1657 ..SchedulerConfig::default_for_testing()
1658 };
1659 let scheduler = ScanScheduler::new(Arc::new(ObjectStore::memory()), config);
1660 let reader = Arc::new(ShortReader {
1661 path: Path::parse("short-file").unwrap(),
1662 });
1663 let file_scheduler = scheduler.open_reader(reader);
1664
1665 let error = file_scheduler
1666 .submit_request(vec![0..8], 0)
1667 .await
1668 .unwrap_err();
1669
1670 assert!(matches!(error, Error::IO { .. }), "{error:?}");
1671 assert!(
1672 error.to_string().contains(
1673 "I/O request for file short-file and range 0..8 returned 0 bytes, expected 8 bytes"
1674 ),
1675 "{error}"
1676 );
1677 }
1678
1679 #[tokio::test]
1680 async fn test_split_coalesce() {
1681 let tmp_file = TempObjFile::default();
1682
1683 let obj_store = Arc::new(ObjectStore::local());
1684
1685 const DATA_SIZE: u64 = 75 * 1024 * 1024;
1687 let mut some_data = vec![0; DATA_SIZE as usize];
1688 rand::rng().fill_bytes(&mut some_data);
1689 obj_store.put(&tmp_file, &some_data).await.unwrap();
1690
1691 let config = SchedulerConfig::default_for_testing();
1692
1693 let scheduler = ScanScheduler::new(obj_store, config);
1694
1695 let file_scheduler = scheduler
1696 .open_file(&tmp_file, &CachedFileSize::unknown())
1697 .await
1698 .unwrap();
1699
1700 let req =
1703 file_scheduler.submit_request(vec![50_000..51_000, 52_000..53_000, 54_000..55_000], 0);
1704
1705 let bytes = req.await.unwrap();
1706
1707 assert_eq!(bytes[0], &some_data[50_000..51_000]);
1708 assert_eq!(bytes[1], &some_data[52_000..53_000]);
1709 assert_eq!(bytes[2], &some_data[54_000..55_000]);
1710
1711 assert_eq!(1, scheduler.stats().iops);
1712
1713 let req = file_scheduler.submit_request(vec![0..DATA_SIZE], 0);
1715 let bytes = req.await.unwrap();
1716 assert!(bytes[0] == some_data, "data is not the same");
1717
1718 assert_eq!(6, scheduler.stats().iops);
1719
1720 let chunk_size = *DEFAULT_MAX_IOP_SIZE;
1724 let req = file_scheduler.submit_request(
1725 vec![
1726 10..chunk_size,
1727 chunk_size + 10..(chunk_size * 2) - 20,
1728 chunk_size * 2..(chunk_size * 2) + 10,
1729 ],
1730 0,
1731 );
1732
1733 let bytes = req.await.unwrap();
1734 let chunk_size = chunk_size as usize;
1735 assert!(
1736 bytes[0] == some_data[10..chunk_size],
1737 "data is not the same"
1738 );
1739 assert!(
1740 bytes[1] == some_data[chunk_size + 10..(chunk_size * 2) - 20],
1741 "data is not the same"
1742 );
1743 assert!(
1744 bytes[2] == some_data[chunk_size * 2..(chunk_size * 2) + 10],
1745 "data is not the same"
1746 );
1747 assert_eq!(8, scheduler.stats().iops);
1748
1749 let reads = (0..44)
1750 .map(|i| i * 1_000_000..(i + 1) * 1_000_000)
1751 .collect::<Vec<_>>();
1752 let req = file_scheduler.submit_request(reads, 0);
1753 let bytes = req.await.unwrap();
1754 for (i, bytes) in bytes.iter().enumerate() {
1755 assert!(
1756 bytes == &some_data[i * 1_000_000..(i + 1) * 1_000_000],
1757 "data is not the same"
1758 );
1759 }
1760 assert_eq!(11, scheduler.stats().iops);
1761
1762 let reads = vec![
1767 50_000..51_000,
1768 52_000..53_000,
1769 150_000..151_000,
1770 151_000..152_000,
1771 ];
1772 for (gap, expected_iops) in [(0, 3), (4 * 1024, 2), (100_000, 1), (u64::MAX, 1)] {
1773 let iops_before = scheduler.stats().iops;
1774 let bytes = file_scheduler
1775 .with_block_size(gap)
1776 .submit_request(reads.clone(), 0)
1777 .await
1778 .unwrap();
1779 assert_eq!(bytes.len(), reads.len(), "gap={gap}");
1780 for (range, bytes) in reads.iter().zip(&bytes) {
1781 let expected = &some_data[range.start as usize..range.end as usize];
1782 assert_eq!(bytes, expected, "gap={gap} range={range:?}");
1783 }
1784 assert_eq!(
1785 scheduler.stats().iops - iops_before,
1786 expected_iops,
1787 "gap={gap}"
1788 );
1789 }
1790 let iops_before = scheduler.stats().iops;
1792 file_scheduler.submit_request(reads, 0).await.unwrap();
1793 assert_eq!(scheduler.stats().iops - iops_before, 2);
1794 }
1795
1796 #[rstest]
1797 #[case::nested(vec![0..15, 5..6, 24..25])]
1798 #[case::overlapping(vec![0..15, 5..20, 24..25])]
1799 #[case::duplicate(vec![0..15, 0..15, 24..25])]
1800 #[case::spanning(vec![0..31, 5..7, 20..25, 40..41])]
1801 #[case::disjoint(vec![0..15, 24..25])]
1802 #[tokio::test]
1803 async fn test_coalesce_overlapping_ranges(
1804 #[case] ranges: Vec<Range<u64>>,
1805 #[values(0, 4, 256 * 1024, u64::MAX)] gap: u64,
1806 #[values(false, true)] use_lite_scheduler: bool,
1807 ) {
1808 let path = Path::parse("overlapping-ranges").unwrap();
1809 let source = (0_u8..64).collect::<Vec<_>>();
1810 let object_store = Arc::new(ObjectStore::memory());
1811 object_store.put(&path, &source).await.unwrap();
1812 let scheduler = ScanScheduler::new(
1813 object_store,
1814 SchedulerConfig {
1815 use_lite_scheduler: Some(use_lite_scheduler),
1816 ..SchedulerConfig::default_for_testing()
1817 },
1818 );
1819 let mut file_scheduler = scheduler
1820 .open_file(&path, &CachedFileSize::unknown())
1821 .await
1822 .unwrap()
1823 .with_block_size(gap);
1824 file_scheduler.max_iop_size = 16;
1826
1827 let bytes = file_scheduler
1828 .submit_request(ranges.clone(), 0)
1829 .await
1830 .unwrap();
1831
1832 assert_eq!(bytes.len(), ranges.len());
1833 for (bytes, range) in bytes.iter().zip(&ranges) {
1834 assert_eq!(
1835 bytes.as_ref(),
1836 &source[range.start as usize..range.end as usize]
1837 );
1838 }
1839 }
1840
1841 #[rstest]
1842 #[case::standard(false)]
1843 #[case::lite(true)]
1844 #[tokio::test]
1845 async fn test_unordered_ranges_are_rejected(#[case] use_lite_scheduler: bool) {
1846 let path = Path::parse("unordered-ranges").unwrap();
1847 let source = (0_u8..64).collect::<Vec<_>>();
1848 let object_store = Arc::new(ObjectStore::memory());
1849 object_store.put(&path, &source).await.unwrap();
1850
1851 let config = SchedulerConfig {
1852 use_lite_scheduler: Some(use_lite_scheduler),
1853 ..SchedulerConfig::default_for_testing()
1854 };
1855 let scheduler = ScanScheduler::new(object_store, config);
1856 let file_scheduler = scheduler
1857 .open_file(&path, &CachedFileSize::unknown())
1858 .await
1859 .unwrap();
1860
1861 let ranges = vec![9..26, 0..49];
1862 let error = file_scheduler.submit_request(ranges, 0).await.unwrap_err();
1863
1864 assert!(matches!(error, Error::InvalidInput { .. }), "{error:?}");
1865 assert!(
1866 error.to_string().contains(
1867 "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"
1868 ),
1869 "{error}"
1870 );
1871 }
1872
1873 #[tokio::test]
1874 async fn test_io_stats_sink() {
1875 let tmp_file = TempObjFile::default();
1876 let obj_store = Arc::new(ObjectStore::local());
1877
1878 const DATA_SIZE: u64 = 1024 * 1024;
1879 let mut some_data = vec![0; DATA_SIZE as usize];
1880 rand::rng().fill_bytes(&mut some_data);
1881 obj_store.put(&tmp_file, &some_data).await.unwrap();
1882
1883 let scheduler = ScanScheduler::new(obj_store, SchedulerConfig::default_for_testing());
1884
1885 let sink = IoStats::new();
1887 let file_scheduler = scheduler
1888 .open_file(&tmp_file, &CachedFileSize::unknown())
1889 .await
1890 .unwrap()
1891 .with_io_stats(sink.recorder());
1892
1893 file_scheduler
1897 .submit_request(vec![50_000..51_000, 52_000..53_000, 54_000..55_000], 0)
1898 .await
1899 .unwrap();
1900
1901 let global = scheduler.stats();
1902 let scoped = sink.snapshot();
1903 assert_eq!(1, scoped.iops);
1904 assert_eq!(1, scoped.requests);
1905 assert_eq!(5000, scoped.bytes_read);
1907 assert_eq!(global.iops, scoped.iops);
1908 assert_eq!(global.requests, scoped.requests);
1909 assert_eq!(global.bytes_read, scoped.bytes_read);
1910
1911 let other = scheduler
1914 .open_file(&tmp_file, &CachedFileSize::unknown())
1915 .await
1916 .unwrap();
1917 other.submit_request(vec![0..1000], 0).await.unwrap();
1918
1919 let global_after = scheduler.stats();
1920 let scoped_after = sink.snapshot();
1921 assert_eq!(global.bytes_read + 1000, global_after.bytes_read);
1922 assert_eq!(scoped.bytes_read, scoped_after.bytes_read);
1923 assert_eq!(scoped.iops, scoped_after.iops);
1924 }
1925
1926 #[tokio::test]
1927 async fn test_priority() {
1928 let some_path = Path::parse("foo").unwrap();
1929 let base_store = Arc::new(InMemory::new());
1930 base_store
1931 .put(&some_path, vec![0; 1000].into())
1932 .await
1933 .unwrap();
1934
1935 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
1936 let mut obj_store = MockObjectStore::default();
1937 let semaphore_copy = semaphore.clone();
1938 obj_store
1939 .expect_get_opts()
1940 .returning(move |location, options| {
1941 let semaphore = semaphore.clone();
1942 let base_store = base_store.clone();
1943 let location = location.clone();
1944 async move {
1945 semaphore.acquire().await.unwrap().forget();
1946 base_store.get_opts(&location, options).await
1947 }
1948 .boxed()
1949 });
1950 let obj_store = Arc::new(ObjectStore::new(
1951 Arc::new(obj_store),
1952 Url::parse("mem://").unwrap(),
1953 Some(500),
1954 None,
1955 false,
1956 false,
1957 1,
1958 DEFAULT_DOWNLOAD_RETRY_COUNT,
1959 None,
1960 ));
1961
1962 let config = SchedulerConfig {
1963 io_buffer_size_bytes: 1024 * 1024,
1964 use_lite_scheduler: None,
1965 };
1966
1967 let scan_scheduler = ScanScheduler::new(obj_store, config);
1968
1969 let file_scheduler = scan_scheduler
1970 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
1971 .await
1972 .unwrap();
1973
1974 let first_fut = timeout(
1978 Duration::from_secs(10),
1979 file_scheduler.submit_single(0..10, 0),
1980 )
1981 .boxed();
1982
1983 let mut second_fut = timeout(
1985 Duration::from_secs(10),
1986 file_scheduler.submit_single(0..20, 100),
1987 )
1988 .boxed();
1989
1990 let mut third_fut = timeout(
1993 Duration::from_secs(10),
1994 file_scheduler.submit_single(0..30, 0),
1995 )
1996 .boxed();
1997
1998 semaphore_copy.add_permits(1);
2000 assert!(first_fut.await.unwrap().unwrap().len() == 10);
2001 assert!(poll!(&mut second_fut).is_pending());
2003 assert!(poll!(&mut third_fut).is_pending());
2004
2005 semaphore_copy.add_permits(1);
2007 assert!(third_fut.await.unwrap().unwrap().len() == 30);
2008 assert!(poll!(&mut second_fut).is_pending());
2009
2010 semaphore_copy.add_permits(1);
2012 assert!(second_fut.await.unwrap().unwrap().len() == 20);
2013 }
2014
2015 #[tokio::test]
2016 async fn test_standard_scheduler_state_tracks_queue_state() {
2017 let some_path = Path::parse("foo").unwrap();
2018 let base_store = Arc::new(InMemory::new());
2019 base_store
2020 .put(&some_path, vec![0; 1000].into())
2021 .await
2022 .unwrap();
2023
2024 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2025 let mut obj_store = MockObjectStore::default();
2026 let semaphore_copy = semaphore.clone();
2027 obj_store
2028 .expect_get_opts()
2029 .returning(move |location, options| {
2030 let semaphore = semaphore.clone();
2031 let base_store = base_store.clone();
2032 let location = location.clone();
2033 async move {
2034 semaphore.acquire().await.unwrap().forget();
2035 base_store.get_opts(&location, options).await
2036 }
2037 .boxed()
2038 });
2039 let obj_store = Arc::new(ObjectStore::new(
2040 Arc::new(obj_store),
2041 Url::parse("mem://").unwrap(),
2042 Some(500),
2043 None,
2044 false,
2045 false,
2046 1,
2047 DEFAULT_DOWNLOAD_RETRY_COUNT,
2048 None,
2049 ));
2050
2051 let scheduler = ScanScheduler::new(
2052 obj_store,
2053 SchedulerConfig {
2054 io_buffer_size_bytes: 1024 * 1024,
2055 use_lite_scheduler: Some(false),
2056 },
2057 );
2058 let file_scheduler = scheduler
2059 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
2060 .await
2061 .unwrap();
2062
2063 let first_fut = timeout(
2064 Duration::from_secs(10),
2065 file_scheduler.submit_single(0..10, 0),
2066 )
2067 .boxed();
2068 let second_fut = timeout(
2069 Duration::from_secs(10),
2070 file_scheduler.submit_single(0..20, 100),
2071 )
2072 .boxed();
2073 let third_fut = timeout(
2074 Duration::from_secs(10),
2075 file_scheduler.submit_single(0..30, 0),
2076 )
2077 .boxed();
2078
2079 let io_queue = match &scheduler.io_queue {
2080 IoQueueType::Standard(io_queue) => io_queue.clone(),
2081 IoQueueType::Lite(_) => unreachable!("test forces the standard scheduler"),
2082 };
2083 let (
2084 io_capacity,
2085 iops_available,
2086 pending_bytes,
2087 bytes_reserved,
2088 priorities_in_flight,
2089 head_task_bytes,
2090 head_task_blocked_by_iops,
2091 head_task_blocked_by_bytes,
2092 ) = timeout(Duration::from_secs(5), async {
2093 loop {
2094 let observed = {
2095 let state = io_queue.state.lock().unwrap();
2096 let active_iops = state.io_capacity.saturating_sub(state.iops_avail);
2097 if active_iops == 1 && state.pending_requests.len() == 2 {
2098 let pending_bytes = state
2099 .pending_requests
2100 .iter()
2101 .map(IoTask::num_bytes)
2102 .sum::<u64>();
2103 let head_task = state.pending_requests.peek().unwrap();
2104 let bypasses_bytes = state.no_backpressure
2105 || head_task.bypass_backpressure
2106 || head_task.priority <= state.priorities_in_flight.min_in_flight();
2107 Some((
2108 state.io_capacity,
2109 state.iops_avail,
2110 pending_bytes,
2111 state.io_buffer_size as i64 - state.bytes_avail,
2112 state.priorities_in_flight.len(),
2113 head_task.num_bytes(),
2114 state.iops_avail == 0,
2115 !bypasses_bytes && head_task.num_bytes() as i64 > state.bytes_avail,
2116 ))
2117 } else {
2118 None
2119 }
2120 };
2121 if let Some(observed) = observed {
2122 break observed;
2123 }
2124 tokio::task::yield_now().await;
2125 }
2126 })
2127 .await
2128 .unwrap();
2129
2130 assert_eq!(io_capacity, 1);
2131 assert_eq!(iops_available, 0);
2132 assert_eq!(pending_bytes, 50);
2133 assert_eq!(bytes_reserved, 10);
2134 assert_eq!(priorities_in_flight, 1);
2135 assert_eq!(head_task_bytes, 30);
2136 assert!(head_task_blocked_by_iops);
2137 assert!(!head_task_blocked_by_bytes);
2138
2139 semaphore_copy.add_permits(3);
2140 assert_eq!(first_fut.await.unwrap().unwrap().len(), 10);
2141 assert_eq!(third_fut.await.unwrap().unwrap().len(), 30);
2142 assert_eq!(second_fut.await.unwrap().unwrap().len(), 20);
2143 }
2144
2145 #[tokio::test(flavor = "multi_thread")]
2146 async fn test_backpressure() {
2147 let some_path = Path::parse("foo").unwrap();
2148 let base_store = Arc::new(InMemory::new());
2149 base_store
2150 .put(&some_path, vec![0; 100000].into())
2151 .await
2152 .unwrap();
2153
2154 let bytes_read = Arc::new(AtomicU64::from(0));
2155 let mut obj_store = MockObjectStore::default();
2156 let bytes_read_copy = bytes_read.clone();
2157 obj_store
2159 .expect_get_opts()
2160 .returning(move |location, options| {
2161 let range = options.range.as_ref().unwrap();
2162 let num_bytes = match range {
2163 GetRange::Bounded(bounded) => bounded.end - bounded.start,
2164 _ => panic!(),
2165 };
2166 bytes_read_copy.fetch_add(num_bytes, Ordering::Release);
2167 let location = location.clone();
2168 let base_store = base_store.clone();
2169 async move { base_store.get_opts(&location, options).await }.boxed()
2170 });
2171 let obj_store = Arc::new(ObjectStore::new(
2172 Arc::new(obj_store),
2173 Url::parse("mem://").unwrap(),
2174 Some(500),
2175 None,
2176 false,
2177 false,
2178 1,
2179 DEFAULT_DOWNLOAD_RETRY_COUNT,
2180 None,
2181 ));
2182
2183 let config = SchedulerConfig {
2184 io_buffer_size_bytes: 10,
2185 use_lite_scheduler: None,
2186 };
2187
2188 let scan_scheduler = ScanScheduler::new(obj_store.clone(), config);
2189
2190 let file_scheduler = scan_scheduler
2191 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(100000))
2192 .await
2193 .unwrap();
2194
2195 let wait_for_idle = || async move {
2196 let handle = Handle::current();
2197 while handle.metrics().num_alive_tasks() != 1 {
2198 tokio::time::sleep(Duration::from_millis(10)).await;
2199 }
2200 };
2201 let wait_for_bytes_read_and_idle = |target_bytes: u64| {
2202 let bytes_read = &bytes_read;
2204 async move {
2205 let bytes_read_copy = bytes_read.clone();
2206 while bytes_read_copy.load(Ordering::Acquire) < target_bytes {
2207 tokio::time::sleep(Duration::from_millis(10)).await;
2208 }
2209 wait_for_idle().await;
2210 }
2211 };
2212
2213 let first_fut = file_scheduler.submit_single(0..5, 0);
2215 let second_fut = file_scheduler.submit_single(0..5, 0);
2217 let third_fut = file_scheduler.submit_single(0..3, 0);
2219 wait_for_bytes_read_and_idle(10).await;
2221
2222 assert_eq!(first_fut.await.unwrap().len(), 5);
2223 wait_for_bytes_read_and_idle(13).await;
2225
2226 let fourth_fut = file_scheduler.submit_single(0..5, 0);
2228 wait_for_bytes_read_and_idle(13).await;
2229
2230 assert_eq!(third_fut.await.unwrap().len(), 3);
2232 wait_for_bytes_read_and_idle(18).await;
2233
2234 assert_eq!(second_fut.await.unwrap().len(), 5);
2235 let fifth_fut = file_scheduler.submit_request(vec![0..3, 90000..90007], 0);
2242 wait_for_bytes_read_and_idle(21).await;
2243
2244 let fifth_bytes = tokio::time::timeout(Duration::from_secs(10), fifth_fut)
2246 .await
2247 .unwrap();
2248 assert_eq!(
2249 fifth_bytes.unwrap().iter().map(|b| b.len()).sum::<usize>(),
2250 10
2251 );
2252
2253 assert_eq!(fourth_fut.await.unwrap().len(), 5);
2255 wait_for_bytes_read_and_idle(28).await;
2256
2257 let config = SchedulerConfig {
2259 io_buffer_size_bytes: 10,
2260 use_lite_scheduler: None,
2261 };
2262
2263 let scan_scheduler = ScanScheduler::new(obj_store, config);
2264 let file_scheduler = scan_scheduler
2265 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(100000))
2266 .await
2267 .unwrap();
2268
2269 let first_fut = file_scheduler.submit_single(0..10, 0);
2270 let second_fut = file_scheduler.submit_single(0..10, 0);
2271
2272 std::thread::sleep(Duration::from_millis(100));
2273 assert_eq!(first_fut.await.unwrap().len(), 10);
2274 assert_eq!(second_fut.await.unwrap().len(), 10);
2275 }
2276
2277 #[derive(Debug)]
2278 struct BlockingReader {
2279 semaphore: Arc<tokio::sync::Semaphore>,
2280 get_range_count: Arc<AtomicU64>,
2281 path: Path,
2282 }
2283
2284 impl lance_core::deepsize::DeepSizeOf for BlockingReader {
2285 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
2286 0
2287 }
2288 }
2289
2290 impl Reader for BlockingReader {
2291 fn path(&self) -> &Path {
2292 &self.path
2293 }
2294
2295 fn block_size(&self) -> usize {
2296 4096
2297 }
2298
2299 fn io_parallelism(&self) -> usize {
2300 1
2301 }
2302
2303 fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
2304 Box::pin(async { Ok(1_000_000) })
2305 }
2306
2307 fn get_range(
2308 &self,
2309 range: Range<usize>,
2310 ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
2311 self.get_range_count.fetch_add(1, Ordering::Release);
2312 let semaphore = self.semaphore.clone();
2313 let num_bytes = range.end - range.start;
2314 Box::pin(async move {
2315 semaphore.acquire().await.unwrap().forget();
2316 Ok(Bytes::from(vec![0u8; num_bytes]))
2317 })
2318 }
2319
2320 fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
2321 Box::pin(async { Ok(Bytes::from(vec![0u8; 1_000_000])) })
2322 }
2323 }
2324
2325 #[tokio::test(flavor = "multi_thread")]
2326 async fn test_same_priority_chunks_continue_after_higher_priority_request() {
2327 let obj_store = Arc::new(ObjectStore::new(
2328 Arc::new(InMemory::new()),
2329 Url::parse("mem://").unwrap(),
2330 Some(4096),
2331 None,
2332 false,
2333 false,
2334 1,
2335 DEFAULT_DOWNLOAD_RETRY_COUNT,
2336 None,
2337 ));
2338 let scheduler = ScanScheduler::new(
2339 obj_store,
2340 SchedulerConfig {
2341 io_buffer_size_bytes: 10,
2342 use_lite_scheduler: Some(false),
2343 },
2344 );
2345 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2346 let reader: Arc<dyn Reader> = Arc::new(BlockingReader {
2347 semaphore: semaphore.clone(),
2348 get_range_count: Arc::new(AtomicU64::new(0)),
2349 path: Path::parse("test").unwrap(),
2350 });
2351
2352 let low_priority =
2353 scheduler.submit_request(reader.clone(), vec![0..6, 100..106], 10, false);
2354 let high_priority = scheduler.submit_request(reader, vec![200..204], 0, false);
2355
2356 semaphore.add_permits(3);
2357 let low_priority = timeout(Duration::from_secs(5), low_priority)
2358 .await
2359 .unwrap()
2360 .unwrap();
2361 assert_eq!(
2362 low_priority.iter().map(|bytes| bytes.len()).sum::<usize>(),
2363 12
2364 );
2365
2366 let high_priority = timeout(Duration::from_secs(5), high_priority)
2367 .await
2368 .unwrap()
2369 .unwrap();
2370 assert_eq!(high_priority[0].len(), 4);
2371 }
2372
2373 #[derive(Debug)]
2375 struct TrackingReader {
2376 get_range_count: Arc<AtomicU64>,
2377 path: Path,
2378 }
2379
2380 impl lance_core::deepsize::DeepSizeOf for TrackingReader {
2381 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
2382 0
2383 }
2384 }
2385
2386 impl Reader for TrackingReader {
2387 fn path(&self) -> &Path {
2388 &self.path
2389 }
2390
2391 fn block_size(&self) -> usize {
2392 4096
2393 }
2394
2395 fn io_parallelism(&self) -> usize {
2396 1
2397 }
2398
2399 fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
2400 Box::pin(async { Ok(1_000_000) })
2401 }
2402
2403 fn get_range(
2404 &self,
2405 range: Range<usize>,
2406 ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
2407 self.get_range_count.fetch_add(1, Ordering::Release);
2408 let num_bytes = range.end - range.start;
2409 Box::pin(async move { Ok(Bytes::from(vec![0u8; num_bytes])) })
2410 }
2411
2412 fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
2413 Box::pin(async { Ok(Bytes::from(vec![0u8; 1_000_000])) })
2414 }
2415 }
2416
2417 #[tokio::test]
2418 async fn test_lite_scheduler_submits_eagerly() {
2419 let obj_store = Arc::new(ObjectStore::memory());
2420 let config = SchedulerConfig::default_for_testing().with_lite_scheduler();
2421 let scheduler = ScanScheduler::new(obj_store, config);
2422
2423 let get_range_count = Arc::new(AtomicU64::new(0));
2424 let reader: Arc<dyn Reader> = Arc::new(TrackingReader {
2425 get_range_count: get_range_count.clone(),
2426 path: Path::parse("test").unwrap(),
2427 });
2428
2429 let fut1 = scheduler.submit_request(reader.clone(), vec![0..100], 0, false);
2432 let fut2 = scheduler.submit_request(reader.clone(), vec![100..200], 10, false);
2433 let fut3 = scheduler.submit_request(reader.clone(), vec![200..300], 20, false);
2434
2435 assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2437
2438 assert_eq!(fut1.await.unwrap()[0].len(), 100);
2440 assert_eq!(fut2.await.unwrap()[0].len(), 100);
2441 assert_eq!(fut3.await.unwrap()[0].len(), 100);
2442 }
2443
2444 #[tokio::test]
2445 async fn test_object_store_selects_scheduler() {
2446 let memory_store = Arc::new(ObjectStore::memory());
2448 assert!(!memory_store.prefers_lite_scheduler());
2449 let config = SchedulerConfig {
2450 io_buffer_size_bytes: 256 * 1024 * 1024,
2451 use_lite_scheduler: None,
2452 };
2453 let scheduler = ScanScheduler::new(memory_store.clone(), config);
2454 assert!(!scheduler.uses_lite_scheduler());
2455
2456 let uring_store = Arc::new(ObjectStore::new(
2458 Arc::new(InMemory::new()),
2459 Url::parse("file+uring:///tmp").unwrap(),
2460 None,
2461 None,
2462 false,
2463 false,
2464 8,
2465 DEFAULT_DOWNLOAD_RETRY_COUNT,
2466 None,
2467 ));
2468 assert!(uring_store.prefers_lite_scheduler());
2469 let config = SchedulerConfig {
2470 io_buffer_size_bytes: 256 * 1024 * 1024,
2471 use_lite_scheduler: None,
2472 };
2473 let scheduler = ScanScheduler::new(uring_store.clone(), config);
2474 assert!(scheduler.uses_lite_scheduler());
2475
2476 let config = SchedulerConfig {
2478 io_buffer_size_bytes: 256 * 1024 * 1024,
2479 use_lite_scheduler: Some(false),
2480 };
2481 let scheduler = ScanScheduler::new(uring_store, config);
2482 assert!(!scheduler.uses_lite_scheduler());
2483
2484 let config = SchedulerConfig {
2486 io_buffer_size_bytes: 256 * 1024 * 1024,
2487 use_lite_scheduler: Some(true),
2488 };
2489 let scheduler = ScanScheduler::new(memory_store, config);
2490 assert!(scheduler.uses_lite_scheduler());
2491 }
2492
2493 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2494 async fn stress_backpressure() {
2495 let some_path = Path::parse("foo").unwrap();
2499 let obj_store = Arc::new(ObjectStore::memory());
2500 obj_store
2501 .put(&some_path, vec![0; 100000].as_slice())
2502 .await
2503 .unwrap();
2504
2505 let config = SchedulerConfig {
2507 io_buffer_size_bytes: 1,
2508 use_lite_scheduler: None,
2509 };
2510 let scan_scheduler = ScanScheduler::new(obj_store.clone(), config);
2511 let file_scheduler = scan_scheduler
2512 .open_file(&some_path, &CachedFileSize::unknown())
2513 .await
2514 .unwrap();
2515
2516 let mut futs = Vec::with_capacity(10000);
2517 for idx in 0..10000 {
2518 futs.push(file_scheduler.submit_single(idx..idx + 1, idx));
2519 }
2520
2521 for fut in futs {
2522 fut.await.unwrap();
2523 }
2524 }
2525
2526 #[tokio::test(flavor = "multi_thread")]
2527 async fn test_zero_buffer_size_no_backpressure() {
2528 let obj_store = Arc::new(ObjectStore::memory());
2531 let config = SchedulerConfig {
2532 io_buffer_size_bytes: 0,
2533 use_lite_scheduler: Some(false),
2534 };
2535 let scheduler = ScanScheduler::new(obj_store, config);
2536
2537 let get_range_count = Arc::new(AtomicU64::new(0));
2538 let reader: Arc<dyn Reader> = Arc::new(TrackingReader {
2539 get_range_count: get_range_count.clone(),
2540 path: Path::parse("test").unwrap(),
2541 });
2542
2543 let fut1 = scheduler.submit_request(reader.clone(), vec![0..1000], 0, false);
2546 let fut2 = scheduler.submit_request(reader.clone(), vec![1000..2000], 1, false);
2547 let fut3 = scheduler.submit_request(reader.clone(), vec![2000..3000], 2, false);
2548
2549 let bytes1 = timeout(Duration::from_secs(5), fut1)
2550 .await
2551 .unwrap()
2552 .unwrap();
2553 let bytes2 = timeout(Duration::from_secs(5), fut2)
2554 .await
2555 .unwrap()
2556 .unwrap();
2557 let bytes3 = timeout(Duration::from_secs(5), fut3)
2558 .await
2559 .unwrap()
2560 .unwrap();
2561 assert_eq!(bytes1[0].len(), 1000);
2562 assert_eq!(bytes2[0].len(), 1000);
2563 assert_eq!(bytes3[0].len(), 1000);
2564 assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2565 }
2566
2567 #[tokio::test(flavor = "multi_thread")]
2568 async fn test_file_scheduler_bypass_backpressure() {
2569 let some_path = Path::parse("foo").unwrap();
2572 let base_store = Arc::new(InMemory::new());
2573 base_store
2574 .put(&some_path, vec![0u8; 1000].into())
2575 .await
2576 .unwrap();
2577
2578 let bytes_dispatched = Arc::new(AtomicU64::from(0));
2579 let mut obj_store = MockObjectStore::default();
2580 let bytes_dispatched_copy = bytes_dispatched.clone();
2581 obj_store
2582 .expect_get_opts()
2583 .returning(move |location, options| {
2584 let range = options.range.as_ref().unwrap();
2585 let num_bytes = match range {
2586 GetRange::Bounded(bounded) => bounded.end - bounded.start,
2587 _ => panic!(),
2588 };
2589 bytes_dispatched_copy.fetch_add(num_bytes, Ordering::Release);
2590 let location = location.clone();
2591 let base_store = base_store.clone();
2592 async move { base_store.get_opts(&location, options).await }.boxed()
2593 });
2594 let obj_store = Arc::new(ObjectStore::new(
2595 Arc::new(obj_store),
2596 Url::parse("mem://").unwrap(),
2597 Some(500),
2598 None,
2599 false,
2600 false,
2601 1,
2602 DEFAULT_DOWNLOAD_RETRY_COUNT,
2603 None,
2604 ));
2605
2606 let config = SchedulerConfig {
2608 io_buffer_size_bytes: 10,
2609 use_lite_scheduler: Some(false),
2610 };
2611 let scan_scheduler = ScanScheduler::new(obj_store, config);
2612 let file_scheduler = scan_scheduler
2613 .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
2614 .await
2615 .unwrap();
2616 let bypass_scheduler = file_scheduler.with_bypass_backpressure();
2617
2618 let blocker_fut = file_scheduler.submit_single(0..10, 0);
2620 while bytes_dispatched.load(Ordering::Acquire) < 10 {
2621 tokio::time::sleep(Duration::from_millis(1)).await;
2622 }
2623
2624 let normal_fut = file_scheduler.submit_single(0..10, 2);
2627 let bypass_fut = bypass_scheduler.submit_single(0..10, 1);
2628
2629 while bytes_dispatched.load(Ordering::Acquire) < 20 {
2631 tokio::time::sleep(Duration::from_millis(1)).await;
2632 }
2633 tokio::time::sleep(Duration::from_millis(20)).await;
2634 assert_eq!(
2635 bytes_dispatched.load(Ordering::Acquire),
2636 20,
2637 "normal read should still be blocked while budget is exhausted"
2638 );
2639
2640 timeout(Duration::from_secs(5), blocker_fut)
2642 .await
2643 .unwrap()
2644 .unwrap();
2645 timeout(Duration::from_secs(5), bypass_fut)
2646 .await
2647 .unwrap()
2648 .unwrap();
2649 timeout(Duration::from_secs(5), normal_fut)
2650 .await
2651 .unwrap()
2652 .unwrap();
2653 assert_eq!(bytes_dispatched.load(Ordering::Acquire), 30);
2654 }
2655
2656 async fn run_caller_drop_scenario(use_lite_scheduler: bool) -> (bool, Duration) {
2662 let obj_store = Arc::new(ObjectStore::new(
2663 Arc::new(InMemory::new()),
2664 Url::parse("mem://").unwrap(),
2665 Some(4096),
2666 None,
2667 false,
2668 false,
2669 1,
2670 DEFAULT_DOWNLOAD_RETRY_COUNT,
2671 None,
2672 ));
2673 let scheduler = ScanScheduler::new(
2674 obj_store,
2675 SchedulerConfig {
2676 io_buffer_size_bytes: 100,
2677 use_lite_scheduler: Some(use_lite_scheduler),
2678 },
2679 );
2680
2681 let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2682 let get_range_count = Arc::new(AtomicU64::new(0));
2683 let reader: Arc<dyn Reader> = Arc::new(BlockingReader {
2684 semaphore: semaphore.clone(),
2685 get_range_count: get_range_count.clone(),
2686 path: Path::parse("test").unwrap(),
2687 });
2688
2689 let fut1 = scheduler.submit_request(reader.clone(), vec![0..50], 0, false);
2693 let handle = tokio::spawn(async move {
2694 let _ = fut1.await;
2695 });
2696
2697 while get_range_count.load(Ordering::Acquire) == 0 {
2701 tokio::time::sleep(Duration::from_millis(1)).await;
2702 }
2703
2704 handle.abort();
2706 let _ = handle.await;
2707
2708 semaphore.add_permits(1);
2711 tokio::time::sleep(Duration::from_millis(50)).await;
2714
2715 semaphore.add_permits(1);
2718 let fut2 = scheduler.submit_request(reader, vec![100..160], 1, false);
2719
2720 let start = std::time::Instant::now();
2721 let outcome = timeout(Duration::from_secs(2), fut2).await;
2722 let elapsed = start.elapsed();
2723 match outcome {
2724 Ok(res) => {
2725 assert_eq!(res.unwrap().iter().map(|b| b.len()).sum::<usize>(), 60);
2726 (true, elapsed)
2727 }
2728 Err(_) => (false, elapsed),
2729 }
2730 }
2731
2732 #[tokio::test(flavor = "multi_thread")]
2736 async fn standard_scheduler_refunds_reservation_on_caller_drop() {
2737 let (completed, elapsed) = run_caller_drop_scenario(false).await;
2738 assert!(
2739 completed,
2740 "standard scheduler deadlocked the follow-up request (elapsed {elapsed:?}); \
2741 the dropped request's reservation was not refunded"
2742 );
2743 }
2744
2745 #[tokio::test(flavor = "multi_thread")]
2748 async fn lite_scheduler_refunds_reservation_on_caller_drop() {
2749 let (completed, elapsed) = run_caller_drop_scenario(true).await;
2750 assert!(
2751 completed,
2752 "lite scheduler deadlocked the follow-up request (elapsed {elapsed:?}); \
2753 the dropped request's reservation was not refunded"
2754 );
2755 }
2756}