Skip to main content

lance_io/
scheduler.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use 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
29// Don't log backpressure warnings until at least this many seconds have passed
30const BACKPRESSURE_MIN: u64 = 5;
31// Don't log backpressure warnings more than once / minute
32const BACKPRESSURE_DEBOUNCE: u64 = 60;
33const SCHEDULER_STATE_EVENT_TARGET: &str = "lance_io::scheduler::state";
34
35// Global counter of how many IOPS we have issued
36static IOPS_COUNTER: AtomicU64 = AtomicU64::new(0);
37// Global counter of how many bytes were read by the scheduler
38static 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
48// We want to allow requests that have a lower priority than any
49// currently in-flight request.  This helps avoid potential deadlocks
50// related to backpressure.  Unfortunately, it is quite expensive to
51// keep track of which priorities are in-flight.
52//
53// TODO: At some point it would be nice if we can optimize this away but
54// in_flight should remain relatively small (generally less than 256 items)
55// and has not shown itself to be a bottleneck yet.
56struct 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    // The configured number of IOPS that can be issued concurrently.
100    io_capacity: u32,
101    // Number of IOPS we can issue concurrently before pausing I/O
102    iops_avail: u32,
103    // The configured byte budget for unread I/O.
104    io_buffer_size: u64,
105    // Number of bytes we are allowed to buffer in memory before pausing I/O
106    //
107    // This can dip below 0 due to I/O prioritization
108    bytes_avail: i64,
109    // Pending I/O requests
110    pending_requests: BinaryHeap<IoTask>,
111    // Priorities of in-flight requests
112    priorities_in_flight: PrioritiesInFlight,
113    // Set when the scheduler is finished to notify the I/O loop to shut down
114    // once all outstanding requests have been completed.
115    done_scheduling: bool,
116    // Time when the scheduler started
117    start: Instant,
118    // Last time we warned about backpressure
119    last_warn: AtomicU64,
120    // When true, skip all byte-based backpressure checks (set when io_buffer_size == 0)
121    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            // Chunks from an admitted logical request must keep moving.  A
238            // higher-priority request may be scheduled later and remain
239            // unconsumed while the caller awaits this request.
240            || 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                    // This can happen when we admit special priority requests
258                    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
271// This is modeled after the MPSC queue described here: https://docs.rs/tokio/latest/tokio/sync/struct.Notify.html
272//
273// However, it only needs to be SPSC since there is only one "scheduler thread"
274// and one I/O loop.
275struct IoQueue {
276    // Queue state
277    state: Mutex<IoQueueState>,
278    // Used to signal new I/O requests have arrived that might potentially be runnable
279    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
371// There is one instance of MutableBatch shared by all the I/O operations
372// that make up a single request.  When all the I/O operations complete
373// then the MutableBatch goes out of scope and the batch request is considered
374// complete
375struct 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    // When true, report 0 bytes consumed so the backpressure budget is unaffected
384    bypass_backpressure: bool,
385    // Queue the batch's backpressure reservation is refunded to once its response
386    // is delivered or discarded (see `Response`'s `Drop`).
387    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
413// Rather than keep track of when all the I/O requests are finished so that we
414// can deliver the batch of data we let Rust do that for us.  When all I/O's are
415// done then the MutableBatch will go out of scope and we know we have all the
416// data.
417impl<F: FnOnce(Response) + Send> Drop for MutableBatch<F> {
418    fn drop(&mut self) {
419        // If we have an error, return that. Otherwise return the data, as long as the I/O requests have been processed.
420        let result = if let Some(err) = self.err.take() {
421            Err(err)
422        } else if self.num_delivered < self.data_buffers.len() {
423            // This usually happens on tokio runtime shutdown
424            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        // We don't really care if no one is around to receive it, just let
435        // the result go out of scope and get cleaned up
436        let response = Response {
437            data: Some(result),
438            io_queue: self.io_queue.clone(),
439            // Report 0 bytes for bypass tasks so the backpressure budget is unaffected
440            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    // Called by worker tasks to add data to the MutableBatch
464    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                // This keeps the original error, if present
473                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        // Bypass tasks are always delivered before normal tasks.
521        // Within the same bypass class, this is a min-heap on priority.
522        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        // Emit per-file I/O trace event only when tracing is enabled
558        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
570// Every time a scheduler starts up it launches a task to run the I/O loop.  This loop
571// repeats endlessly until the scheduler is destroyed.
572async fn run_io_loop(tasks: Arc<IoQueue>) {
573    // Pop the first finished task off the queue and submit another until
574    // we are done
575    loop {
576        let next_task = tasks.pop().await;
577        match next_task {
578            Some(task) => {
579                tokio::spawn(task.run());
580            }
581            None => {
582                // The sender has been dropped, we are done
583                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    /// Add already-aggregated counts (e.g. a snapshot captured from another
627    /// scheduler) into these counters.
628    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        // Inherent methods take precedence in resolution, so this delegates to
638        // the inherent `record_request` above rather than recursing.
639        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/// A shareable, cloneable handle to a set of cumulative I/O counters.
739///
740/// All clones share the same underlying counters.  This serves two purposes:
741///
742/// 1. It backs each [`ScanScheduler`]'s own running totals.
743/// 2. It can be attached to an individual [`FileScheduler`] (via
744///    [`FileScheduler::with_io_stats`]) as a *secondary* sink, so a caller can
745///    measure the exact bytes/IOPS performed through that file handle for a
746///    bounded scope (e.g. a single query) without disturbing the scheduler's
747///    global totals.  Read the result back with [`IoStats::snapshot`].
748#[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    /// Record a single completed request.  `request` holds the byte ranges as
757    /// actually submitted to storage (post coalescing/splitting), so the counts
758    /// reflect physical I/O.
759    pub fn record_request(&self, request: &[Range<u64>]) {
760        self.0.record_request(request);
761    }
762
763    /// Take an immutable snapshot of the current cumulative counters.
764    pub fn snapshot(&self) -> ScanStats {
765        ScanStats::new(self.0.as_ref())
766    }
767
768    /// Return this handle as a type-erased [`IoStatsRecorder`], suitable for
769    /// attaching to a file reader (e.g. `FileReader::with_io_stats`).  The
770    /// returned recorder shares the same underlying counters as `self`.
771    pub fn recorder(&self) -> Arc<dyn IoStatsRecorder> {
772        self.0.clone()
773    }
774
775    /// Add a snapshot of already-aggregated statistics into this sink.  Used to
776    /// fold in I/O measured on a separate scheduler (e.g. the one-time reads
777    /// performed while opening an index).
778    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
794/// An I/O scheduler which wraps an ObjectStore and throttles the amount of
795/// parallel I/O that can be run.
796///
797/// The ScanScheduler will cancel any outstanding I/O requests when it is dropped.
798/// For this reason it should be kept alive until all I/O has finished.
799///
800/// Note: The 2.X file readers already do this so this is only a concern if you are
801/// using the ScanScheduler directly.
802pub 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    // `Option` so the caller can take the data out while the response (and its
818    // backpressure refund on drop) stays intact.
819    data: Option<Result<Vec<Bytes>>>,
820    io_queue: Arc<IoQueue>,
821    priority: u128,
822    num_reqs: usize,
823    num_bytes: u64,
824}
825
826// Refund the batch's backpressure reservation when the response is dropped, be
827// that on delivery or when a cancelled request's undelivered response is
828// discarded.  This releases the budget even if the caller drops the future early.
829impl 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/// Bytes [`SchedulerConfig::max_bandwidth`] allows per I/O thread, sized to hold one
837/// storage page of the largest size we write.
838const MAX_BANDWIDTH_BYTES_PER_IO_THREAD: u64 = 32 * 1024 * 1024;
839
840#[derive(Debug, Clone, Copy)]
841pub struct SchedulerConfig {
842    /// the # of bytes that can be buffered but not yet requested.
843    /// This controls back pressure.  If data is not processed quickly enough then this
844    /// buffer will fill up and the I/O loop will pause until the buffer is drained.
845    pub io_buffer_size_bytes: u64,
846    /// Whether to use the lite scheduler.
847    ///
848    /// - `Some(true)` forces the lite scheduler (e.g. from env var or programmatic).
849    /// - `Some(false)` forces the standard scheduler.
850    /// - `None` defers to the object store's preference (see [`ObjectStore::prefers_lite_scheduler`]).
851    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    /// Big enough for unit testing
865    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    /// Configuration that should generally maximize bandwidth (not trying to save RAM
873    /// at all).  We assume a max page size of 32MiB and then allow 32MiB per I/O thread
874    pub fn max_bandwidth(store: &ObjectStore) -> Self {
875        Self::new(Self::max_bandwidth_bytes(store.io_parallelism()))
876    }
877
878    /// The byte budget [`Self::max_bandwidth`] would give a store with this I/O parallelism,
879    /// for callers that must reason about it without holding the store — to divide it between
880    /// several schedulers, say.
881    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    /// Create a new scheduler with the given I/O capacity
895    ///
896    /// # Arguments
897    ///
898    /// * object_store - the store to wrap
899    /// * config - configuration settings for the scheduler
900    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            // Best we can do here is fire and forget.  If the I/O loop is still running when the scheduler is
921            // dropped we can't wait for it to finish or we'd block a tokio thread.  We could spawn a blocking task
922            // to wait for it to finish but that doesn't seem helpful.
923            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    /// Open a file for reading
934    ///
935    /// # Arguments
936    ///
937    /// * path - the path to the file to open
938    /// * base_priority - the base priority for I/O requests submitted to this file scheduler
939    ///   this will determine the upper 64 bits of priority (the lower 64 bits
940    ///   come from `submit_request` and `submit_single`)
941    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    /// Open a file with a default priority of 0
974    ///
975    /// See [`Self::open_file_with_priority`] for more information on the priority
976    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    /// Open a [`FileScheduler`] over an already-open [`Reader`].
985    ///
986    /// Unlike [`Self::open_file`], this skips the path lookup and size probe and
987    /// schedules I/O against `reader` directly. This is useful when the reader
988    /// was produced outside the scheduler's object store (e.g. a spill file
989    /// opened via [`crate::spill::Spill::reader`]), since a bare `Reader`
990    /// cannot otherwise drive a v2 `FileReader` (which needs a scheduler).
991    ///
992    /// Uses a base priority of 0; chain [`FileScheduler::with_priority`] to set
993    /// a different one.
994    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            // We don't care if the receiver has given up so discard the result
1019            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            // A cancel error can't occur: the sender always sends before dropping.
1069            // The reservation is refunded on `Response` drop, so just take the data.
1070            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        // It's important that we submit all requests _before_ we await anything
1084        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                    // An empty range asks for no bytes, which IoTask::run
1092                    // answers without a request. Reaching a cloud store with
1093                    // one is an InvalidGetRange error there.
1094                    if requested_range.start == requested_range.end {
1095                        return async { Ok(Bytes::new()) }.boxed();
1096                    }
1097                    // IoTask::run publishes the counters behind
1098                    // iops_counter()/bytes_read_counter(), so this path has to
1099                    // publish the same pair the same way: the requested
1100                    // length, on the error arm as well.
1101                    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        // If the user is dropping the ScanScheduler then they _should_ be done with I/O.  This can happen
1166        // even when I/O is in progress if, for example, the user is dropping a scan mid-read because they found
1167        // the data they wanted (limit after filter or some other example).
1168        //
1169        // Closing the I/O queue will cancel any requests that have not yet been sent to the I/O loop.  However,
1170        // it will not terminate the I/O loop itself.  This is to help prevent deadlock and ensure that all I/O
1171        // requests that are submitted will terminate.
1172        //
1173        // In theory, this isn't strictly necessary, as callers should drop any task expecting I/O before they
1174        // drop the scheduler.  In practice, this can be difficult to do, and it is better to spend a little bit
1175        // of time letting the I/O loop drain so that we can avoid any potential deadlocks.
1176        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/// A throttled file reader
1184#[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    /// Optional secondary statistics sink.  When set, every request submitted
1193    /// through this handle is also recorded here, in addition to the
1194    /// scheduler's global totals.  Used to measure per-scope I/O.
1195    extra_stats: Option<Arc<dyn IoStatsRecorder>>,
1196}
1197
1198fn is_close_together(range1: &Range<u64>, range2: &Range<u64>, block_size: u64) -> bool {
1199    // Note that range1.end <= range2.start is possible (e.g. when decoding string arrays)
1200    // A gap from `FileScheduler::with_block_size` may be as large as `u64::MAX`.
1201    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    /// Submit a batch of I/O requests to the reader
1210    ///
1211    /// The requests will be queued in a FIFO manner and, when all requests
1212    /// have been fulfilled, the returned future will be completed.
1213    ///
1214    /// Each request has a given priority.  If the I/O loop is full then requests
1215    /// will be buffered and requests with the *lowest* priority will be released
1216    /// from the buffer first.
1217    ///
1218    /// Each request has a backpressure ID which controls which backpressure throttle
1219    /// is applied to the request.  Requests made to the same backpressure throttle
1220    /// will be throttled together.
1221    ///
1222    /// Ranges must be sorted by their start offset.
1223    pub fn submit_request(
1224        &self,
1225        request: Vec<Range<u64>>,
1226        priority: u64,
1227    ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1228        // The final priority is a combination of the row offset and the file number
1229        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                        // Last request is a bit bigger due to rounding
1272                        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                    // We need to undo the coalescing and splitting done earlier
1307                    let start = orig_range.start as usize - byte_offset;
1308                    if orig_range.end <= updated_range.end {
1309                        // The original range is fully contained in the updated range, can do
1310                        // zero-copy slice
1311                        let end = orig_range.end as usize - byte_offset;
1312                        final_bytes.push(bytes_vec[updated_index].slice(start..end));
1313                    } else {
1314                        // The original read was split into multiple requests, need to copy
1315                        // back into a single buffer
1316                        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                        // Later ranges may overlap this one, so keep their search
1321                        // cursor at this range's first chunk while copying its tail.
1322                        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    /// Returns a copy of this scheduler that additionally records the I/O it
1358    /// performs into `stats`, on top of the scheduler's global statistics.
1359    ///
1360    /// This is the mechanism for measuring exact per-scope (e.g. per-query) I/O:
1361    /// attach a recorder here (e.g. via [`IoStats::recorder`]), perform the reads
1362    /// through the returned handle, then read the totals back with
1363    /// [`IoStats::snapshot`].  The returned handle is cheap to create (a few
1364    /// `Arc` clones) and reuses the same underlying reader, so it does not
1365    /// re-open the file.
1366    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    /// Returns a copy of this scheduler that merges the ranges of a request
1374    /// that are at most `block_size` bytes apart into one I/O, instead of
1375    /// using the object store's block size.
1376    ///
1377    /// Each requested range is still cut back out of the merged read, so the
1378    /// bytes returned are the same for every gap; only the number and size of
1379    /// the I/Os change. A wider gap trades extra bytes for fewer requests,
1380    /// which pays off where every request is a round trip to remote storage.
1381    pub fn with_block_size(&self, block_size: u64) -> Self {
1382        Self {
1383            block_size,
1384            ..self.clone()
1385        }
1386    }
1387
1388    /// Returns a copy of this scheduler that bypasses backpressure for all requests.
1389    ///
1390    /// This should be used for indirect I/O (e.g. fetching items after decoding offsets) where
1391    /// blocking on backpressure could cause a deadlock or excessive latency.
1392    pub fn with_bypass_backpressure(&self) -> Self {
1393        Self {
1394            bypass_backpressure: true,
1395            ..self.clone()
1396        }
1397    }
1398
1399    /// Submit a single IOP to the reader
1400    ///
1401    /// If you have multiple IOPS to perform then [`Self::submit_request`] is going
1402    /// to be more efficient.
1403    ///
1404    /// See [`Self::submit_request`] for more information on the priority and backpressure.
1405    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    /// Provides access to the underlying reader
1415    ///
1416    /// Do not use this for reading data as it will bypass any I/O scheduling!
1417    /// This is mainly exposed to allow metadata operations (e.g size, block_size,)
1418    /// which either aren't IOPS or we don't throttle
1419    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        // Bypass tasks must come out of the heap before non-bypass tasks.
1501        // Within each group, lower priority number (= higher priority) comes first.
1502        let mut heap = BinaryHeap::new();
1503        heap.push(make_task(10, false)); // non-bypass, low priority
1504        heap.push(make_task(1, false)); // non-bypass, high priority
1505        heap.push(make_task(20, true)); // bypass, low priority
1506        heap.push(make_task(5, true)); // bypass, high priority
1507
1508        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, // num_data_buffers
1523            0, // priority
1524            2, // num_reqs
1525            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        // Write 1MiB of data
1545        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        // Read it back 4KiB at a time
1560        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        // Note: we should get parallel I/O even though we are consuming serially
1576        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        // Open a bare Reader ourselves, then bridge it into a FileScheduler.
1600        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        // Write 75MiB of data
1686        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        // These 3 requests should be coalesced into a single I/O because they are within 4KiB
1701        // of each other
1702        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        // This should be split into 5 requests because it is so large
1714        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        // None of these requests are bigger than the max IOP size but they will be coalesced into
1721        // one IOP that is bigger and then split back into 2 requests that don't quite align with the original
1722        // ranges.
1723        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        // A gap set on the file scheduler replaces the store's 4KiB block
1763        // size: gap 0 merges only touching ranges, a wider gap merges every
1764        // range it reaches, and `u64::MAX` merges them all. Each range still
1765        // gets exactly its own bytes.
1766        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        // The scheduler the gap was set on keeps the store's block size.
1791        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        // Preserve the production split/reassembly boundary with a tiny fixture.
1825        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        // Attach a per-scope sink to one file handle.
1886        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        // Three reads within 4KiB coalesce into a single physical IOP.  The sink
1894        // and the scheduler's global totals must agree exactly, because both are
1895        // recorded from the same post-coalescing request.
1896        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        // Coalesced range 50_000..55_000 => 5000 physical bytes.
1906        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        // A sibling handle without the sink: the global totals advance but the
1912        // sink stays put, proving per-scope isolation.
1913        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        // Issue a request, priority doesn't matter, it will be submitted
1975        // immediately (it will go pending)
1976        // Note: the timeout is to prevent a deadlock if the test fails.
1977        let first_fut = timeout(
1978            Duration::from_secs(10),
1979            file_scheduler.submit_single(0..10, 0),
1980        )
1981        .boxed();
1982
1983        // Issue another low priority request (it will go in queue)
1984        let mut second_fut = timeout(
1985            Duration::from_secs(10),
1986            file_scheduler.submit_single(0..20, 100),
1987        )
1988        .boxed();
1989
1990        // Issue a high priority request (it will go in queue and should bump
1991        // the other queued request down)
1992        let mut third_fut = timeout(
1993            Duration::from_secs(10),
1994            file_scheduler.submit_single(0..30, 0),
1995        )
1996        .boxed();
1997
1998        // Finish one file, should be the in-flight first request
1999        semaphore_copy.add_permits(1);
2000        assert!(first_fut.await.unwrap().unwrap().len() == 10);
2001        // Other requests should not be finished
2002        assert!(poll!(&mut second_fut).is_pending());
2003        assert!(poll!(&mut third_fut).is_pending());
2004
2005        // Next should be high priority request
2006        semaphore_copy.add_permits(1);
2007        assert!(third_fut.await.unwrap().unwrap().len() == 30);
2008        assert!(poll!(&mut second_fut).is_pending());
2009
2010        // Finally, the low priority request
2011        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        // Wraps the obj_store to keep track of how many bytes have been read
2158        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            // We need to move `target` but don't want to move `bytes_read`
2203            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        // This read will begin immediately
2214        let first_fut = file_scheduler.submit_single(0..5, 0);
2215        // This read should also begin immediately
2216        let second_fut = file_scheduler.submit_single(0..5, 0);
2217        // This read will be throttled
2218        let third_fut = file_scheduler.submit_single(0..3, 0);
2219        // Two tasks (third_fut and unit test)
2220        wait_for_bytes_read_and_idle(10).await;
2221
2222        assert_eq!(first_fut.await.unwrap().len(), 5);
2223        // One task (unit test)
2224        wait_for_bytes_read_and_idle(13).await;
2225
2226        // 2 bytes are ready but 5 bytes requested, read will be blocked
2227        let fourth_fut = file_scheduler.submit_single(0..5, 0);
2228        wait_for_bytes_read_and_idle(13).await;
2229
2230        // Out of order completion is ok, will unblock backpressure
2231        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        // At this point there are 5 bytes available in backpressure queue
2236        // Now we issue multi-read that can be partially fulfilled, it will read some bytes but
2237        // not all of them. (using large range gap to ensure request not coalesced)
2238        //
2239        // I'm actually not sure this behavior is great.  It's possible that we should just
2240        // block until we can fulfill the entire request.
2241        let fifth_fut = file_scheduler.submit_request(vec![0..3, 90000..90007], 0);
2242        wait_for_bytes_read_and_idle(21).await;
2243
2244        // Fifth future should eventually finish due to deadlock prevention
2245        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        // And now let's just make sure that we can read the rest of the data
2254        assert_eq!(fourth_fut.await.unwrap().len(), 5);
2255        wait_for_bytes_read_and_idle(28).await;
2256
2257        // Ensure deadlock prevention timeout can be disabled
2258        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    /// A Reader that tracks how many times get_range has been called.
2374    #[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        // Submit several requests. The lite scheduler should call get_range
2430        // eagerly during submit (before the returned future is polled).
2431        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        // get_range must have been called for all 3 requests already.
2436        assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2437
2438        // The futures should still resolve with the correct data.
2439        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        // A memory:// store should use the standard scheduler when config is None
2447        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        // A file+uring:// store should use the lite scheduler when config is None
2457        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        // Explicit Some(false) overrides a file+uring:// store's preference
2477        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        // Explicit Some(true) overrides a memory:// store's preference
2485        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        // This test ensures that the backpressure mechanism works correctly with
2496        // regards to priority.  In other words, as long as all requests are consumed
2497        // in priority order then the backpressure mechanism should not deadlock
2498        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        // Only one request will be allowed in
2506        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        // With io_buffer_size_bytes=0 (no_backpressure=true), reads at any priority go
2529        // through without blocking, even though a zero budget would normally halt all I/O.
2530        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        // Submit three reads at increasing priorities without awaiting any first.
2544        // Priority 1 and 2 would deadlock under a real 0-byte budget without no_backpressure.
2545        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        // A FileScheduler obtained via with_bypass_backpressure() submits reads that bypass
2570        // the byte budget, allowing them to proceed even when the budget is exhausted.
2571        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        // Budget = 10 bytes.
2607        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        // Fill the 10-byte budget with a priority-0 read.
2619        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        // A normal read at priority 2 is blocked: budget = 0, priority 2 > min-in-flight 0.
2625        // A bypass read at priority 1 (higher priority in the queue) bypasses the budget check.
2626        let normal_fut = file_scheduler.submit_single(0..10, 2);
2627        let bypass_fut = bypass_scheduler.submit_single(0..10, 1);
2628
2629        // Bypass read is dispatched; normal read is still blocked.
2630        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        // Consuming the blocker releases its 10-byte budget → normal read can proceed.
2641        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    // Against a 100-byte budget: submit fut1 (50 bytes, priority 0), drop it while
2657    // its read is still blocked in get_range, then submit fut2 (60 bytes, priority 1).
2658    // fut2's priority can't win the priority-bypass, so it needs 60 of the budget --
2659    // available only if fut1's dropped reservation was refunded. Returns whether fut2
2660    // completed within 2s (false = the reservation leaked and fut2 deadlocked).
2661    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        // Step 1: reserve 50 of the 100 budget bytes with a read we never consume.
2690        // Spawn it so we can cancel the caller-side future while it is still parked
2691        // waiting for the (blocked) read to finish.
2692        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        // Wait until the read is genuinely in flight (blocked on the semaphore).
2698        // This guarantees the 50-byte reservation has been taken before we drop
2699        // the caller, closing the race between the I/O loop and the abort.
2700        while get_range_count.load(Ordering::Acquire) == 0 {
2701            tokio::time::sleep(Duration::from_millis(1)).await;
2702        }
2703
2704        // Step 2: drop the caller-side future while its `rx` is still pending.
2705        handle.abort();
2706        let _ = handle.await;
2707
2708        // Step 3: let the in-flight read finish. The reservation should be refunded
2709        // now that the request is done, whether or not the caller is still around.
2710        semaphore.add_permits(1);
2711        // Give the read time to run to completion so the refund would already have
2712        // happened.
2713        tokio::time::sleep(Duration::from_millis(50)).await;
2714
2715        // Step 4: submit the follow-up. Add a permit up front so that, if it *is*
2716        // admitted, its own read can complete rather than block on the semaphore.
2717        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    /// Dropping a standard-scheduler request future while its read is in flight must
2733    /// still refund the backpressure reservation, so a later request that needs the
2734    /// budget does not deadlock.
2735    #[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    /// Same guarantee for the lite scheduler: dropping a request future mid-read
2746    /// releases its reservation via the `TaskHandle` drop path.
2747    #[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}