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::{FutureExt, TryFutureExt};
7use object_store::path::Path;
8use std::collections::BinaryHeap;
9use std::fmt::Debug;
10use std::future::Future;
11use std::num::NonZero;
12use std::ops::Range;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::Instant;
16use tokio::sync::Notify;
17
18use lance_core::utils::io_stats::IoStatsRecorder;
19use lance_core::utils::parse::str_is_truthy;
20use lance_core::{Error, Result};
21
22use crate::object_store::ObjectStore;
23use crate::traits::Reader;
24use crate::utils::CachedFileSize;
25
26mod lite;
27
28// Don't log backpressure warnings until at least this many seconds have passed
29const BACKPRESSURE_MIN: u64 = 5;
30// Don't log backpressure warnings more than once / minute
31const BACKPRESSURE_DEBOUNCE: u64 = 60;
32const SCHEDULER_STATE_EVENT_TARGET: &str = "lance_io::scheduler::state";
33
34// Global counter of how many IOPS we have issued
35static IOPS_COUNTER: AtomicU64 = AtomicU64::new(0);
36// Global counter of how many bytes were read by the scheduler
37static BYTES_READ_COUNTER: AtomicU64 = AtomicU64::new(0);
38
39pub fn iops_counter() -> u64 {
40    IOPS_COUNTER.load(Ordering::Acquire)
41}
42
43pub fn bytes_read_counter() -> u64 {
44    BYTES_READ_COUNTER.load(Ordering::Acquire)
45}
46
47// We want to allow requests that have a lower priority than any
48// currently in-flight request.  This helps avoid potential deadlocks
49// related to backpressure.  Unfortunately, it is quite expensive to
50// keep track of which priorities are in-flight.
51//
52// TODO: At some point it would be nice if we can optimize this away but
53// in_flight should remain relatively small (generally less than 256 items)
54// and has not shown itself to be a bottleneck yet.
55struct PrioritiesInFlight {
56    in_flight: Vec<u128>,
57}
58
59impl PrioritiesInFlight {
60    fn new(capacity: u32) -> Self {
61        Self {
62            in_flight: Vec::with_capacity(capacity as usize * 2),
63        }
64    }
65
66    fn min_in_flight(&self) -> u128 {
67        self.in_flight.first().copied().unwrap_or(u128::MAX)
68    }
69
70    fn contains(&self, prio: u128) -> bool {
71        self.in_flight.binary_search(&prio).is_ok()
72    }
73
74    fn push(&mut self, prio: u128) {
75        let pos = match self.in_flight.binary_search(&prio) {
76            Ok(pos) => pos,
77            Err(pos) => pos,
78        };
79        self.in_flight.insert(pos, prio);
80    }
81
82    fn remove(&mut self, prio: u128) {
83        if let Ok(pos) = self.in_flight.binary_search(&prio) {
84            self.in_flight.remove(pos);
85        }
86    }
87
88    fn len(&self) -> usize {
89        self.in_flight.len()
90    }
91
92    fn is_empty(&self) -> bool {
93        self.in_flight.is_empty()
94    }
95}
96
97struct IoQueueState {
98    // The configured number of IOPS that can be issued concurrently.
99    io_capacity: u32,
100    // Number of IOPS we can issue concurrently before pausing I/O
101    iops_avail: u32,
102    // The configured byte budget for unread I/O.
103    io_buffer_size: u64,
104    // Number of bytes we are allowed to buffer in memory before pausing I/O
105    //
106    // This can dip below 0 due to I/O prioritization
107    bytes_avail: i64,
108    // Pending I/O requests
109    pending_requests: BinaryHeap<IoTask>,
110    // Priorities of in-flight requests
111    priorities_in_flight: PrioritiesInFlight,
112    // Set when the scheduler is finished to notify the I/O loop to shut down
113    // once all outstanding requests have been completed.
114    done_scheduling: bool,
115    // Time when the scheduler started
116    start: Instant,
117    // Last time we warned about backpressure
118    last_warn: AtomicU64,
119    // When true, skip all byte-based backpressure checks (set when io_buffer_size == 0)
120    no_backpressure: bool,
121}
122
123impl IoQueueState {
124    fn new(io_capacity: u32, io_buffer_size: u64) -> Self {
125        Self {
126            io_capacity,
127            iops_avail: io_capacity,
128            io_buffer_size,
129            bytes_avail: io_buffer_size as i64,
130            pending_requests: BinaryHeap::new(),
131            priorities_in_flight: PrioritiesInFlight::new(io_capacity),
132            done_scheduling: false,
133            start: Instant::now(),
134            last_warn: AtomicU64::from(0),
135            no_backpressure: io_buffer_size == 0,
136        }
137    }
138
139    fn scheduler_state_event(&self) -> Option<SchedulerStateEvent> {
140        if !tracing::enabled!(target: SCHEDULER_STATE_EVENT_TARGET, tracing::Level::TRACE) {
141            return None;
142        }
143
144        let pending_bytes = self
145            .pending_requests
146            .iter()
147            .map(IoTask::num_bytes)
148            .sum::<u64>();
149        let head_task = self.pending_requests.peek();
150        let min_in_flight_priority = if self.priorities_in_flight.is_empty() {
151            None
152        } else {
153            Some(self.priorities_in_flight.min_in_flight())
154        };
155        let head_task_priority_bypass = head_task.map(|task| {
156            self.no_backpressure
157                || task.bypass_backpressure
158                || task.priority <= self.priorities_in_flight.min_in_flight()
159        });
160        let head_task_blocked_by_iops = head_task.map(|_| self.iops_avail == 0);
161        let head_task_blocked_by_bytes = head_task.map(|task| {
162            let bypasses_bytes = self.no_backpressure
163                || task.bypass_backpressure
164                || task.priority <= self.priorities_in_flight.min_in_flight();
165            !bypasses_bytes && task.num_bytes() as i64 > self.bytes_avail
166        });
167        let head_task_can_deliver = head_task.map(|task| self.can_deliver_without_warning(task));
168        let head_task_bytes = head_task.map(IoTask::num_bytes);
169        let (head_task_priority_high, head_task_priority_low) =
170            split_priority(head_task.map(|task| task.priority));
171        let (min_in_flight_priority_high, min_in_flight_priority_low) =
172            split_priority(min_in_flight_priority);
173
174        Some(SchedulerStateEvent {
175            queue_kind: "standard",
176            io_capacity: u64::from(self.io_capacity),
177            iops_available: u64::from(self.iops_avail),
178            active_iops: u64::from(self.io_capacity.saturating_sub(self.iops_avail)),
179            pending_iops: self.pending_requests.len() as u64,
180            pending_bytes,
181            bytes_available: self.bytes_avail,
182            bytes_reserved: self.io_buffer_size as i64 - self.bytes_avail,
183            io_buffer_size_bytes: self.io_buffer_size,
184            priorities_in_flight: self.priorities_in_flight.len() as u64,
185            no_backpressure: self.no_backpressure,
186            head_task_bytes,
187            head_task_priority_high,
188            head_task_priority_low,
189            min_in_flight_priority_high,
190            min_in_flight_priority_low,
191            head_task_can_deliver,
192            head_task_priority_bypass,
193            head_task_blocked_by_iops,
194            head_task_blocked_by_bytes,
195        })
196    }
197
198    fn warn_if_needed(&self) {
199        let seconds_elapsed = self.start.elapsed().as_secs();
200        let last_warn = self.last_warn.load(Ordering::Acquire);
201        let since_last_warn = seconds_elapsed - last_warn;
202        if (last_warn == 0
203            && seconds_elapsed > BACKPRESSURE_MIN
204            && seconds_elapsed < BACKPRESSURE_DEBOUNCE)
205            || since_last_warn > BACKPRESSURE_DEBOUNCE
206        {
207            tracing::event!(tracing::Level::DEBUG, "Backpressure throttle exceeded");
208            log::debug!(
209                "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"
210            );
211            self.last_warn
212                .store(seconds_elapsed.max(1), Ordering::Release);
213        }
214    }
215
216    fn can_deliver(&self, task: &IoTask) -> bool {
217        let can_deliver = self.can_deliver_without_warning(task);
218        if !can_deliver
219            && self.iops_avail > 0
220            && !(self.no_backpressure
221                || task.bypass_backpressure
222                || task.priority <= self.priorities_in_flight.min_in_flight())
223            && task.num_bytes() as i64 > self.bytes_avail
224        {
225            self.warn_if_needed();
226        }
227        can_deliver
228    }
229
230    fn can_deliver_without_warning(&self, task: &IoTask) -> bool {
231        if self.iops_avail == 0 {
232            false
233        } else if self.no_backpressure
234            || task.bypass_backpressure
235            || task.priority <= self.priorities_in_flight.min_in_flight()
236            // Chunks from an admitted logical request must keep moving.  A
237            // higher-priority request may be scheduled later and remain
238            // unconsumed while the caller awaits this request.
239            || self.priorities_in_flight.contains(task.priority)
240        {
241            true
242        } else {
243            task.num_bytes() as i64 <= self.bytes_avail
244        }
245    }
246
247    fn next_task(&mut self) -> Option<IoTask> {
248        let task = self.pending_requests.peek()?;
249        if self.can_deliver(task) {
250            let skip_bytes_accounting = self.no_backpressure || task.bypass_backpressure;
251            self.priorities_in_flight.push(task.priority);
252            self.iops_avail -= 1;
253            if !skip_bytes_accounting {
254                self.bytes_avail -= task.num_bytes() as i64;
255                if self.bytes_avail < 0 {
256                    // This can happen when we admit special priority requests
257                    log::debug!(
258                        "Backpressure throttle temporarily exceeded by {} bytes due to priority I/O",
259                        -self.bytes_avail
260                    );
261                }
262            }
263            Some(self.pending_requests.pop().unwrap())
264        } else {
265            None
266        }
267    }
268}
269
270// This is modeled after the MPSC queue described here: https://docs.rs/tokio/latest/tokio/sync/struct.Notify.html
271//
272// However, it only needs to be SPSC since there is only one "scheduler thread"
273// and one I/O loop.
274struct IoQueue {
275    // Queue state
276    state: Mutex<IoQueueState>,
277    // Used to signal new I/O requests have arrived that might potentially be runnable
278    notify: Notify,
279    stats: IoStats,
280}
281
282impl IoQueue {
283    fn new(io_capacity: u32, io_buffer_size: u64, stats: IoStats) -> Self {
284        Self {
285            state: Mutex::new(IoQueueState::new(io_capacity, io_buffer_size)),
286            notify: Notify::new(),
287            stats,
288        }
289    }
290
291    fn push(&self, task: IoTask) {
292        log::trace!(
293            "Inserting I/O request for {} bytes with priority ({},{}) into I/O queue",
294            task.num_bytes(),
295            task.priority >> 64,
296            task.priority & 0xFFFFFFFFFFFFFFFF
297        );
298        let event = {
299            let mut state = self.state.lock().unwrap();
300            state.pending_requests.push(task);
301            state.scheduler_state_event()
302        };
303        emit_scheduler_state_event(event, &self.stats);
304
305        self.notify.notify_one();
306    }
307
308    async fn pop(&self) -> Option<IoTask> {
309        loop {
310            {
311                let mut state = self.state.lock().unwrap();
312                if let Some(task) = state.next_task() {
313                    let event = state.scheduler_state_event();
314                    drop(state);
315                    emit_scheduler_state_event(event, &self.stats);
316                    return Some(task);
317                }
318
319                if state.done_scheduling {
320                    return None;
321                }
322            }
323
324            self.notify.notified().await;
325        }
326    }
327
328    fn on_iop_complete(&self) {
329        let event = {
330            let mut state = self.state.lock().unwrap();
331            state.iops_avail += 1;
332            state.scheduler_state_event()
333        };
334        emit_scheduler_state_event(event, &self.stats);
335
336        self.notify.notify_one();
337    }
338
339    fn on_bytes_consumed(&self, bytes: u64, priority: u128, num_reqs: usize) {
340        let event = {
341            let mut state = self.state.lock().unwrap();
342            state.bytes_avail += bytes as i64;
343            for _ in 0..num_reqs {
344                state.priorities_in_flight.remove(priority);
345            }
346            state.scheduler_state_event()
347        };
348        emit_scheduler_state_event(event, &self.stats);
349
350        self.notify.notify_one();
351    }
352
353    fn close(&self) {
354        let (pending_requests, event) = {
355            let mut state = self.state.lock().unwrap();
356            state.done_scheduling = true;
357            let pending_requests = std::mem::take(&mut state.pending_requests);
358            let event = state.scheduler_state_event();
359            (pending_requests, event)
360        };
361        emit_scheduler_state_event(event, &self.stats);
362        for request in pending_requests {
363            request.cancel();
364        }
365
366        self.notify.notify_one();
367    }
368}
369
370// There is one instance of MutableBatch shared by all the I/O operations
371// that make up a single request.  When all the I/O operations complete
372// then the MutableBatch goes out of scope and the batch request is considered
373// complete
374struct MutableBatch<F: FnOnce(Response) + Send> {
375    when_done: Option<F>,
376    data_buffers: Vec<Bytes>,
377    num_bytes: u64,
378    priority: u128,
379    num_reqs: usize,
380    err: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
381    // When true, report 0 bytes consumed so the backpressure budget is unaffected
382    bypass_backpressure: bool,
383    // Queue the batch's backpressure reservation is refunded to once its response
384    // is delivered or discarded (see `Response`'s `Drop`).
385    io_queue: Arc<IoQueue>,
386}
387
388impl<F: FnOnce(Response) + Send> MutableBatch<F> {
389    fn new(
390        when_done: F,
391        num_data_buffers: u32,
392        priority: u128,
393        num_reqs: usize,
394        bypass_backpressure: bool,
395        io_queue: Arc<IoQueue>,
396    ) -> Self {
397        Self {
398            when_done: Some(when_done),
399            data_buffers: vec![Bytes::default(); num_data_buffers as usize],
400            num_bytes: 0,
401            priority,
402            num_reqs,
403            err: None,
404            bypass_backpressure,
405            io_queue,
406        }
407    }
408}
409
410// Rather than keep track of when all the I/O requests are finished so that we
411// can deliver the batch of data we let Rust do that for us.  When all I/O's are
412// done then the MutableBatch will go out of scope and we know we have all the
413// data.
414impl<F: FnOnce(Response) + Send> Drop for MutableBatch<F> {
415    fn drop(&mut self) {
416        // If we have an error, return that.  Otherwise return the data
417        let result = if self.err.is_some() {
418            Err(Error::wrapped(self.err.take().unwrap()))
419        } else {
420            let mut data = Vec::new();
421            std::mem::swap(&mut data, &mut self.data_buffers);
422            Ok(data)
423        };
424        // We don't really care if no one is around to receive it, just let
425        // the result go out of scope and get cleaned up
426        let response = Response {
427            data: Some(result),
428            io_queue: self.io_queue.clone(),
429            // Report 0 bytes for bypass tasks so the backpressure budget is unaffected
430            num_bytes: if self.bypass_backpressure {
431                0
432            } else {
433                self.num_bytes
434            },
435            priority: self.priority,
436            num_reqs: self.num_reqs,
437        };
438        (self.when_done.take().unwrap())(response);
439    }
440}
441
442struct DataChunk {
443    task_idx: usize,
444    num_bytes: u64,
445    data: Result<Bytes>,
446}
447
448trait DataSink: Send {
449    fn deliver_data(&mut self, data: DataChunk);
450}
451
452impl<F: FnOnce(Response) + Send> DataSink for MutableBatch<F> {
453    // Called by worker tasks to add data to the MutableBatch
454    fn deliver_data(&mut self, data: DataChunk) {
455        self.num_bytes += data.num_bytes;
456        match data.data {
457            Ok(data_bytes) => {
458                self.data_buffers[data.task_idx] = data_bytes;
459            }
460            Err(err) => {
461                // This keeps the original error, if present
462                self.err.get_or_insert(Box::new(err));
463            }
464        }
465    }
466}
467
468struct IoTask {
469    reader: Arc<dyn Reader>,
470    to_read: Range<u64>,
471    when_done: Box<dyn FnOnce(Result<Bytes>) + Send>,
472    priority: u128,
473    bypass_backpressure: bool,
474}
475
476impl Eq for IoTask {}
477
478impl PartialEq for IoTask {
479    fn eq(&self, other: &Self) -> bool {
480        self.bypass_backpressure == other.bypass_backpressure && self.priority == other.priority
481    }
482}
483
484impl PartialOrd for IoTask {
485    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
486        Some(self.cmp(other))
487    }
488}
489
490impl Ord for IoTask {
491    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
492        // Bypass tasks are always delivered before normal tasks.
493        // Within the same bypass class, this is a min-heap on priority.
494        self.bypass_backpressure
495            .cmp(&other.bypass_backpressure)
496            .then(other.priority.cmp(&self.priority))
497    }
498}
499
500impl IoTask {
501    fn num_bytes(&self) -> u64 {
502        self.to_read.end - self.to_read.start
503    }
504    fn cancel(self) {
505        (self.when_done)(Err(Error::internal(
506            "Scheduler closed before I/O was completed".to_string(),
507        )));
508    }
509
510    async fn run(self) {
511        let file_path = self.reader.path().as_ref();
512        let num_bytes = self.num_bytes();
513        let bytes = if self.to_read.start == self.to_read.end {
514            Ok(Bytes::new())
515        } else {
516            let bytes_fut = self
517                .reader
518                .get_range(self.to_read.start as usize..self.to_read.end as usize);
519            IOPS_COUNTER.fetch_add(1, Ordering::Release);
520            let num_bytes = self.num_bytes();
521            bytes_fut
522                .inspect(move |_| {
523                    BYTES_READ_COUNTER.fetch_add(num_bytes, Ordering::Release);
524                })
525                .await
526                .map_err(Error::from)
527        };
528        // Emit per-file I/O trace event only when tracing is enabled
529        tracing::trace!(
530            file = file_path,
531            bytes_read = num_bytes,
532            requests = 1,
533            range_start = self.to_read.start,
534            range_end = self.to_read.end,
535            "File I/O completed"
536        );
537        (self.when_done)(bytes);
538    }
539}
540
541// Every time a scheduler starts up it launches a task to run the I/O loop.  This loop
542// repeats endlessly until the scheduler is destroyed.
543async fn run_io_loop(tasks: Arc<IoQueue>) {
544    // Pop the first finished task off the queue and submit another until
545    // we are done
546    loop {
547        let next_task = tasks.pop().await;
548        match next_task {
549            Some(task) => {
550                tokio::spawn(task.run());
551            }
552            None => {
553                // The sender has been dropped, we are done
554                return;
555            }
556        }
557    }
558}
559
560#[derive(Debug)]
561struct StatsCollector {
562    iops: AtomicU64,
563    requests: AtomicU64,
564    bytes_read: AtomicU64,
565}
566
567impl StatsCollector {
568    fn new() -> Self {
569        Self {
570            iops: AtomicU64::new(0),
571            requests: AtomicU64::new(0),
572            bytes_read: AtomicU64::new(0),
573        }
574    }
575
576    fn iops(&self) -> u64 {
577        self.iops.load(Ordering::Relaxed)
578    }
579
580    fn bytes_read(&self) -> u64 {
581        self.bytes_read.load(Ordering::Relaxed)
582    }
583
584    fn requests(&self) -> u64 {
585        self.requests.load(Ordering::Relaxed)
586    }
587
588    fn record_request(&self, request: &[Range<u64>]) {
589        self.requests.fetch_add(1, Ordering::Relaxed);
590        self.iops.fetch_add(request.len() as u64, Ordering::Relaxed);
591        self.bytes_read.fetch_add(
592            request.iter().map(|r| r.end - r.start).sum::<u64>(),
593            Ordering::Relaxed,
594        );
595    }
596
597    /// Add already-aggregated counts (e.g. a snapshot captured from another
598    /// scheduler) into these counters.
599    fn add(&self, iops: u64, requests: u64, bytes_read: u64) {
600        self.iops.fetch_add(iops, Ordering::Relaxed);
601        self.requests.fetch_add(requests, Ordering::Relaxed);
602        self.bytes_read.fetch_add(bytes_read, Ordering::Relaxed);
603    }
604}
605
606impl IoStatsRecorder for StatsCollector {
607    fn record_request(&self, request: &[Range<u64>]) {
608        // Inherent methods take precedence in resolution, so this delegates to
609        // the inherent `record_request` above rather than recursing.
610        Self::record_request(self, request)
611    }
612}
613
614#[derive(Debug, Clone, Copy, Default)]
615pub struct ScanStats {
616    pub iops: u64,
617    pub requests: u64,
618    pub bytes_read: u64,
619}
620
621impl ScanStats {
622    fn new(stats: &StatsCollector) -> Self {
623        Self {
624            iops: stats.iops(),
625            requests: stats.requests(),
626            bytes_read: stats.bytes_read(),
627        }
628    }
629}
630
631fn split_priority(priority: Option<u128>) -> (Option<u64>, Option<u64>) {
632    priority
633        .map(|priority| ((priority >> 64) as u64, priority as u64))
634        .unzip()
635}
636
637#[derive(Debug, Clone, Copy)]
638pub(super) struct SchedulerStateEvent {
639    pub(super) queue_kind: &'static str,
640    pub(super) io_capacity: u64,
641    pub(super) iops_available: u64,
642    pub(super) active_iops: u64,
643    pub(super) pending_iops: u64,
644    pub(super) pending_bytes: u64,
645    pub(super) bytes_available: i64,
646    pub(super) bytes_reserved: i64,
647    pub(super) io_buffer_size_bytes: u64,
648    pub(super) priorities_in_flight: u64,
649    pub(super) no_backpressure: bool,
650    pub(super) head_task_bytes: Option<u64>,
651    pub(super) head_task_priority_high: Option<u64>,
652    pub(super) head_task_priority_low: Option<u64>,
653    pub(super) min_in_flight_priority_high: Option<u64>,
654    pub(super) min_in_flight_priority_low: Option<u64>,
655    pub(super) head_task_can_deliver: Option<bool>,
656    pub(super) head_task_priority_bypass: Option<bool>,
657    pub(super) head_task_blocked_by_iops: Option<bool>,
658    pub(super) head_task_blocked_by_bytes: Option<bool>,
659}
660
661impl SchedulerStateEvent {
662    fn trace(self, stats: ScanStats) {
663        tracing::event!(
664            target: SCHEDULER_STATE_EVENT_TARGET,
665            tracing::Level::TRACE,
666            queue_kind = self.queue_kind,
667            scheduler_iops = stats.iops,
668            scheduler_requests = stats.requests,
669            scheduler_bytes_read = stats.bytes_read,
670            io_capacity = self.io_capacity,
671            iops_available = self.iops_available,
672            active_iops = self.active_iops,
673            pending_iops = self.pending_iops,
674            pending_bytes = self.pending_bytes,
675            bytes_available = self.bytes_available,
676            bytes_reserved = self.bytes_reserved,
677            io_buffer_size_bytes = self.io_buffer_size_bytes,
678            priorities_in_flight = self.priorities_in_flight,
679            no_backpressure = self.no_backpressure,
680            head_task_bytes_present = self.head_task_bytes.is_some(),
681            head_task_bytes = self.head_task_bytes.unwrap_or_default(),
682            head_task_priority_high_present = self.head_task_priority_high.is_some(),
683            head_task_priority_high = self.head_task_priority_high.unwrap_or_default(),
684            head_task_priority_low_present = self.head_task_priority_low.is_some(),
685            head_task_priority_low = self.head_task_priority_low.unwrap_or_default(),
686            min_in_flight_priority_high_present = self.min_in_flight_priority_high.is_some(),
687            min_in_flight_priority_high = self.min_in_flight_priority_high.unwrap_or_default(),
688            min_in_flight_priority_low_present = self.min_in_flight_priority_low.is_some(),
689            min_in_flight_priority_low = self.min_in_flight_priority_low.unwrap_or_default(),
690            head_task_can_deliver_present = self.head_task_can_deliver.is_some(),
691            head_task_can_deliver = self.head_task_can_deliver.unwrap_or(false),
692            head_task_priority_bypass_present = self.head_task_priority_bypass.is_some(),
693            head_task_priority_bypass = self.head_task_priority_bypass.unwrap_or(false),
694            head_task_blocked_by_iops_present = self.head_task_blocked_by_iops.is_some(),
695            head_task_blocked_by_iops = self.head_task_blocked_by_iops.unwrap_or(false),
696            head_task_blocked_by_bytes_present = self.head_task_blocked_by_bytes.is_some(),
697            head_task_blocked_by_bytes = self.head_task_blocked_by_bytes.unwrap_or(false),
698            "Scheduler state"
699        );
700    }
701}
702
703pub(super) fn emit_scheduler_state_event(event: Option<SchedulerStateEvent>, stats: &IoStats) {
704    if let Some(event) = event {
705        event.trace(stats.snapshot());
706    }
707}
708
709/// A shareable, cloneable handle to a set of cumulative I/O counters.
710///
711/// All clones share the same underlying counters.  This serves two purposes:
712///
713/// 1. It backs each [`ScanScheduler`]'s own running totals.
714/// 2. It can be attached to an individual [`FileScheduler`] (via
715///    [`FileScheduler::with_io_stats`]) as a *secondary* sink, so a caller can
716///    measure the exact bytes/IOPS performed through that file handle for a
717///    bounded scope (e.g. a single query) without disturbing the scheduler's
718///    global totals.  Read the result back with [`IoStats::snapshot`].
719#[derive(Debug, Clone)]
720pub struct IoStats(Arc<StatsCollector>);
721
722impl IoStats {
723    pub fn new() -> Self {
724        Self(Arc::new(StatsCollector::new()))
725    }
726
727    /// Record a single completed request.  `request` holds the byte ranges as
728    /// actually submitted to storage (post coalescing/splitting), so the counts
729    /// reflect physical I/O.
730    pub fn record_request(&self, request: &[Range<u64>]) {
731        self.0.record_request(request);
732    }
733
734    /// Take an immutable snapshot of the current cumulative counters.
735    pub fn snapshot(&self) -> ScanStats {
736        ScanStats::new(self.0.as_ref())
737    }
738
739    /// Return this handle as a type-erased [`IoStatsRecorder`], suitable for
740    /// attaching to a file reader (e.g. `FileReader::with_io_stats`).  The
741    /// returned recorder shares the same underlying counters as `self`.
742    pub fn recorder(&self) -> Arc<dyn IoStatsRecorder> {
743        self.0.clone()
744    }
745
746    /// Add a snapshot of already-aggregated statistics into this sink.  Used to
747    /// fold in I/O measured on a separate scheduler (e.g. the one-time reads
748    /// performed while opening an index).
749    pub fn add_scan_stats(&self, stats: &ScanStats) {
750        self.0.add(stats.iops, stats.requests, stats.bytes_read);
751    }
752}
753
754impl Default for IoStats {
755    fn default() -> Self {
756        Self::new()
757    }
758}
759
760enum IoQueueType {
761    Standard(Arc<IoQueue>),
762    Lite(Arc<lite::IoQueue>),
763}
764
765/// An I/O scheduler which wraps an ObjectStore and throttles the amount of
766/// parallel I/O that can be run.
767///
768/// The ScanScheduler will cancel any outstanding I/O requests when it is dropped.
769/// For this reason it should be kept alive until all I/O has finished.
770///
771/// Note: The 2.X file readers already do this so this is only a concern if you are
772/// using the ScanScheduler directly.
773pub struct ScanScheduler {
774    object_store: Arc<ObjectStore>,
775    io_queue: IoQueueType,
776    stats: IoStats,
777}
778
779impl Debug for ScanScheduler {
780    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781        f.debug_struct("ScanScheduler")
782            .field("object_store", &self.object_store)
783            .finish()
784    }
785}
786
787struct Response {
788    // `Option` so the caller can take the data out while the response (and its
789    // backpressure refund on drop) stays intact.
790    data: Option<Result<Vec<Bytes>>>,
791    io_queue: Arc<IoQueue>,
792    priority: u128,
793    num_reqs: usize,
794    num_bytes: u64,
795}
796
797// Refund the batch's backpressure reservation when the response is dropped, be
798// that on delivery or when a cancelled request's undelivered response is
799// discarded.  This releases the budget even if the caller drops the future early.
800impl Drop for Response {
801    fn drop(&mut self) {
802        self.io_queue
803            .on_bytes_consumed(self.num_bytes, self.priority, self.num_reqs);
804    }
805}
806
807#[derive(Debug, Clone, Copy)]
808pub struct SchedulerConfig {
809    /// the # of bytes that can be buffered but not yet requested.
810    /// This controls back pressure.  If data is not processed quickly enough then this
811    /// buffer will fill up and the I/O loop will pause until the buffer is drained.
812    pub io_buffer_size_bytes: u64,
813    /// Whether to use the lite scheduler.
814    ///
815    /// - `Some(true)` forces the lite scheduler (e.g. from env var or programmatic).
816    /// - `Some(false)` forces the standard scheduler.
817    /// - `None` defers to the object store's preference (see [`ObjectStore::prefers_lite_scheduler`]).
818    pub use_lite_scheduler: Option<bool>,
819}
820
821impl SchedulerConfig {
822    pub fn new(io_buffer_size_bytes: u64) -> Self {
823        Self {
824            io_buffer_size_bytes,
825            use_lite_scheduler: std::env::var("LANCE_USE_LITE_SCHEDULER")
826                .ok()
827                .map(|v| str_is_truthy(v.trim())),
828        }
829    }
830
831    /// Big enough for unit testing
832    pub fn default_for_testing() -> Self {
833        Self {
834            io_buffer_size_bytes: 256 * 1024 * 1024,
835            use_lite_scheduler: None,
836        }
837    }
838
839    /// Configuration that should generally maximize bandwidth (not trying to save RAM
840    /// at all).  We assume a max page size of 32MiB and then allow 32MiB per I/O thread
841    pub fn max_bandwidth(store: &ObjectStore) -> Self {
842        Self::new(32 * 1024 * 1024 * store.io_parallelism() as u64)
843    }
844
845    pub fn with_lite_scheduler(self) -> Self {
846        Self {
847            use_lite_scheduler: Some(true),
848            ..self
849        }
850    }
851}
852
853impl ScanScheduler {
854    /// Create a new scheduler with the given I/O capacity
855    ///
856    /// # Arguments
857    ///
858    /// * object_store - the store to wrap
859    /// * config - configuration settings for the scheduler
860    pub fn new(object_store: Arc<ObjectStore>, config: SchedulerConfig) -> Arc<Self> {
861        let io_capacity = object_store.io_parallelism();
862        let stats = IoStats::new();
863        let use_lite = config
864            .use_lite_scheduler
865            .unwrap_or_else(|| object_store.prefers_lite_scheduler());
866        let io_queue = if use_lite {
867            let io_queue = Arc::new(lite::IoQueue::new(
868                io_capacity as u64,
869                config.io_buffer_size_bytes,
870                stats.clone(),
871            ));
872            IoQueueType::Lite(io_queue)
873        } else {
874            let io_queue = Arc::new(IoQueue::new(
875                io_capacity as u32,
876                config.io_buffer_size_bytes,
877                stats.clone(),
878            ));
879            let io_queue_clone = io_queue.clone();
880            // Best we can do here is fire and forget.  If the I/O loop is still running when the scheduler is
881            // dropped we can't wait for it to finish or we'd block a tokio thread.  We could spawn a blocking task
882            // to wait for it to finish but that doesn't seem helpful.
883            tokio::task::spawn(async move { run_io_loop(io_queue_clone).await });
884            IoQueueType::Standard(io_queue)
885        };
886        Arc::new(Self {
887            object_store,
888            io_queue,
889            stats,
890        })
891    }
892
893    /// Open a file for reading
894    ///
895    /// # Arguments
896    ///
897    /// * path - the path to the file to open
898    /// * base_priority - the base priority for I/O requests submitted to this file scheduler
899    ///   this will determine the upper 64 bits of priority (the lower 64 bits
900    ///   come from `submit_request` and `submit_single`)
901    pub async fn open_file_with_priority(
902        self: &Arc<Self>,
903        path: &Path,
904        base_priority: u64,
905        file_size_bytes: &CachedFileSize,
906    ) -> Result<FileScheduler> {
907        let file_size_bytes = if let Some(size) = file_size_bytes.get() {
908            u64::from(size)
909        } else {
910            let size = self.object_store.size(path).await?;
911            if let Some(size) = NonZero::new(size) {
912                file_size_bytes.set(size);
913            }
914            size
915        };
916        let reader = self
917            .object_store
918            .open_with_size(path, file_size_bytes as usize)
919            .await?;
920        let block_size = self.object_store.block_size() as u64;
921        let max_iop_size = self.object_store.max_iop_size();
922        Ok(FileScheduler {
923            reader: reader.into(),
924            block_size,
925            root: self.clone(),
926            base_priority,
927            max_iop_size,
928            bypass_backpressure: false,
929            extra_stats: None,
930        })
931    }
932
933    /// Open a file with a default priority of 0
934    ///
935    /// See [`Self::open_file_with_priority`] for more information on the priority
936    pub async fn open_file(
937        self: &Arc<Self>,
938        path: &Path,
939        file_size_bytes: &CachedFileSize,
940    ) -> Result<FileScheduler> {
941        self.open_file_with_priority(path, 0, file_size_bytes).await
942    }
943
944    /// Open a [`FileScheduler`] over an already-open [`Reader`].
945    ///
946    /// Unlike [`Self::open_file`], this skips the path lookup and size probe and
947    /// schedules I/O against `reader` directly. This is useful when the reader
948    /// was produced outside the scheduler's object store (e.g. a spill file
949    /// opened via [`crate::spill::Spill::reader`]), since a bare `Reader`
950    /// cannot otherwise drive a v2 `FileReader` (which needs a scheduler).
951    ///
952    /// Uses a base priority of 0; chain [`FileScheduler::with_priority`] to set
953    /// a different one.
954    pub fn open_reader(self: &Arc<Self>, reader: Arc<dyn Reader>) -> FileScheduler {
955        FileScheduler {
956            reader,
957            block_size: self.object_store.block_size() as u64,
958            root: self.clone(),
959            base_priority: 0,
960            max_iop_size: self.object_store.max_iop_size(),
961            bypass_backpressure: false,
962            extra_stats: None,
963        }
964    }
965
966    fn do_submit_request(
967        &self,
968        reader: Arc<dyn Reader>,
969        request: Vec<Range<u64>>,
970        tx: oneshot::Sender<Response>,
971        priority: u128,
972        io_queue: &Arc<IoQueue>,
973        bypass_backpressure: bool,
974    ) {
975        let num_iops = request.len() as u32;
976
977        let when_all_io_done = move |bytes_and_permits| {
978            // We don't care if the receiver has given up so discard the result
979            let _ = tx.send(bytes_and_permits);
980        };
981
982        let dest = Arc::new(Mutex::new(Box::new(MutableBatch::new(
983            when_all_io_done,
984            num_iops,
985            priority,
986            request.len(),
987            bypass_backpressure,
988            io_queue.clone(),
989        ))));
990
991        for (task_idx, iop) in request.into_iter().enumerate() {
992            let dest = dest.clone();
993            let io_queue_clone = io_queue.clone();
994            let num_bytes = iop.end - iop.start;
995            let task = IoTask {
996                reader: reader.clone(),
997                to_read: iop,
998                priority,
999                bypass_backpressure,
1000                when_done: Box::new(move |data| {
1001                    io_queue_clone.on_iop_complete();
1002                    let mut dest = dest.lock().unwrap();
1003                    let chunk = DataChunk {
1004                        data,
1005                        task_idx,
1006                        num_bytes,
1007                    };
1008                    dest.deliver_data(chunk);
1009                }),
1010            };
1011            io_queue.push(task);
1012        }
1013    }
1014
1015    fn submit_request_standard(
1016        &self,
1017        reader: Arc<dyn Reader>,
1018        request: Vec<Range<u64>>,
1019        priority: u128,
1020        io_queue: &Arc<IoQueue>,
1021        bypass_backpressure: bool,
1022    ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1023        let (tx, rx) = oneshot::channel::<Response>();
1024
1025        self.do_submit_request(reader, request, tx, priority, io_queue, bypass_backpressure);
1026
1027        rx.map(|wrapped_rsp| {
1028            // A cancel error can't occur: the sender always sends before dropping.
1029            // The reservation is refunded on `Response` drop, so just take the data.
1030            let mut rsp = wrapped_rsp.unwrap();
1031            rsp.data.take().unwrap()
1032        })
1033    }
1034
1035    fn submit_request_lite(
1036        &self,
1037        reader: Arc<dyn Reader>,
1038        request: Vec<Range<u64>>,
1039        priority: u128,
1040        io_queue: &Arc<lite::IoQueue>,
1041        bypass_backpressure: bool,
1042    ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1043        // It's important that we submit all requests _before_ we await anything
1044        let maybe_tasks = request
1045            .into_iter()
1046            .map(|task| {
1047                let reader = reader.clone();
1048                let queue = io_queue.clone();
1049                let run_fn = Box::new(move || {
1050                    reader
1051                        .get_range(task.start as usize..task.end as usize)
1052                        .map_err(Error::from)
1053                        .boxed()
1054                });
1055                queue.submit(task, priority, run_fn, bypass_backpressure)
1056            })
1057            .collect::<Result<Vec<_>>>();
1058        match maybe_tasks {
1059            Ok(tasks) => async move {
1060                let mut results = Vec::with_capacity(tasks.len());
1061                for task in tasks {
1062                    results.push(task.await?);
1063                }
1064                Ok(results)
1065            }
1066            .boxed(),
1067            Err(e) => async move { Err(e) }.boxed(),
1068        }
1069    }
1070
1071    pub fn submit_request(
1072        &self,
1073        reader: Arc<dyn Reader>,
1074        request: Vec<Range<u64>>,
1075        priority: u128,
1076        bypass_backpressure: bool,
1077    ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1078        match &self.io_queue {
1079            IoQueueType::Standard(io_queue) => {
1080                futures::future::Either::Left(self.submit_request_standard(
1081                    reader,
1082                    request,
1083                    priority,
1084                    io_queue,
1085                    bypass_backpressure,
1086                ))
1087            }
1088            IoQueueType::Lite(io_queue) => futures::future::Either::Right(
1089                self.submit_request_lite(reader, request, priority, io_queue, bypass_backpressure),
1090            ),
1091        }
1092    }
1093
1094    pub fn stats(&self) -> ScanStats {
1095        self.stats.snapshot()
1096    }
1097
1098    #[cfg(test)]
1099    fn uses_lite_scheduler(&self) -> bool {
1100        matches!(self.io_queue, IoQueueType::Lite(_))
1101    }
1102}
1103
1104impl Drop for ScanScheduler {
1105    fn drop(&mut self) {
1106        // If the user is dropping the ScanScheduler then they _should_ be done with I/O.  This can happen
1107        // even when I/O is in progress if, for example, the user is dropping a scan mid-read because they found
1108        // the data they wanted (limit after filter or some other example).
1109        //
1110        // Closing the I/O queue will cancel any requests that have not yet been sent to the I/O loop.  However,
1111        // it will not terminate the I/O loop itself.  This is to help prevent deadlock and ensure that all I/O
1112        // requests that are submitted will terminate.
1113        //
1114        // In theory, this isn't strictly necessary, as callers should drop any task expecting I/O before they
1115        // drop the scheduler.  In practice, this can be difficult to do, and it is better to spend a little bit
1116        // of time letting the I/O loop drain so that we can avoid any potential deadlocks.
1117        match &self.io_queue {
1118            IoQueueType::Standard(io_queue) => io_queue.close(),
1119            IoQueueType::Lite(io_queue) => io_queue.close(),
1120        }
1121    }
1122}
1123
1124/// A throttled file reader
1125#[derive(Clone, Debug)]
1126pub struct FileScheduler {
1127    reader: Arc<dyn Reader>,
1128    root: Arc<ScanScheduler>,
1129    block_size: u64,
1130    base_priority: u64,
1131    max_iop_size: u64,
1132    bypass_backpressure: bool,
1133    /// Optional secondary statistics sink.  When set, every request submitted
1134    /// through this handle is also recorded here, in addition to the
1135    /// scheduler's global totals.  Used to measure per-scope I/O.
1136    extra_stats: Option<Arc<dyn IoStatsRecorder>>,
1137}
1138
1139fn is_close_together(range1: &Range<u64>, range2: &Range<u64>, block_size: u64) -> bool {
1140    // Note that range1.end <= range2.start is possible (e.g. when decoding string arrays)
1141    range2.start <= (range1.end + block_size)
1142}
1143
1144fn is_overlapping(range1: &Range<u64>, range2: &Range<u64>) -> bool {
1145    range1.start < range2.end && range2.start < range1.end
1146}
1147
1148impl FileScheduler {
1149    /// Submit a batch of I/O requests to the reader
1150    ///
1151    /// The requests will be queued in a FIFO manner and, when all requests
1152    /// have been fulfilled, the returned future will be completed.
1153    ///
1154    /// Each request has a given priority.  If the I/O loop is full then requests
1155    /// will be buffered and requests with the *lowest* priority will be released
1156    /// from the buffer first.
1157    ///
1158    /// Each request has a backpressure ID which controls which backpressure throttle
1159    /// is applied to the request.  Requests made to the same backpressure throttle
1160    /// will be throttled together.
1161    pub fn submit_request(
1162        &self,
1163        request: Vec<Range<u64>>,
1164        priority: u64,
1165    ) -> impl Future<Output = Result<Vec<Bytes>>> + Send + use<> {
1166        // The final priority is a combination of the row offset and the file number
1167        let priority = ((self.base_priority as u128) << 64) + priority as u128;
1168
1169        let mut merged_requests = Vec::with_capacity(request.len());
1170
1171        if !request.is_empty() {
1172            let mut curr_interval = request[0].clone();
1173
1174            for req in request.iter().skip(1) {
1175                if is_close_together(&curr_interval, req, self.block_size) {
1176                    curr_interval.end = curr_interval.end.max(req.end);
1177                } else {
1178                    merged_requests.push(curr_interval);
1179                    curr_interval = req.clone();
1180                }
1181            }
1182
1183            merged_requests.push(curr_interval);
1184        }
1185
1186        let mut updated_requests = Vec::with_capacity(merged_requests.len());
1187        for req in merged_requests {
1188            if req.is_empty() {
1189                updated_requests.push(req);
1190            } else {
1191                let num_requests = (req.end - req.start).div_ceil(self.max_iop_size);
1192                let bytes_per_request = (req.end - req.start) / num_requests;
1193                for i in 0..num_requests {
1194                    let start = req.start + i * bytes_per_request;
1195                    let end = if i == num_requests - 1 {
1196                        // Last request is a bit bigger due to rounding
1197                        req.end
1198                    } else {
1199                        start + bytes_per_request
1200                    };
1201                    updated_requests.push(start..end);
1202                }
1203            }
1204        }
1205
1206        self.root.stats.record_request(&updated_requests);
1207        if let Some(extra_stats) = &self.extra_stats {
1208            extra_stats.record_request(&updated_requests);
1209        }
1210
1211        let bytes_vec_fut = self.root.submit_request(
1212            self.reader.clone(),
1213            updated_requests.clone(),
1214            priority,
1215            self.bypass_backpressure,
1216        );
1217
1218        let mut updated_index = 0;
1219        let mut final_bytes = Vec::with_capacity(request.len());
1220
1221        async move {
1222            let bytes_vec = bytes_vec_fut.await?;
1223
1224            let mut orig_index = 0;
1225            while (updated_index < updated_requests.len()) && (orig_index < request.len()) {
1226                let updated_range = &updated_requests[updated_index];
1227                let orig_range = &request[orig_index];
1228                let byte_offset = updated_range.start as usize;
1229
1230                if is_overlapping(updated_range, orig_range) {
1231                    // We need to undo the coalescing and splitting done earlier
1232                    let start = orig_range.start as usize - byte_offset;
1233                    if orig_range.end <= updated_range.end {
1234                        // The original range is fully contained in the updated range, can do
1235                        // zero-copy slice
1236                        let end = orig_range.end as usize - byte_offset;
1237                        final_bytes.push(bytes_vec[updated_index].slice(start..end));
1238                    } else {
1239                        // The original read was split into multiple requests, need to copy
1240                        // back into a single buffer
1241                        let orig_size = orig_range.end - orig_range.start;
1242                        let mut merged_bytes = Vec::with_capacity(orig_size as usize);
1243                        merged_bytes.extend_from_slice(&bytes_vec[updated_index].slice(start..));
1244                        let mut copy_offset = merged_bytes.len() as u64;
1245                        while copy_offset < orig_size {
1246                            updated_index += 1;
1247                            let next_range = &updated_requests[updated_index];
1248                            let bytes_to_take =
1249                                (orig_size - copy_offset).min(next_range.end - next_range.start);
1250                            merged_bytes.extend_from_slice(
1251                                &bytes_vec[updated_index].slice(0..bytes_to_take as usize),
1252                            );
1253                            copy_offset += bytes_to_take;
1254                        }
1255                        final_bytes.push(Bytes::from(merged_bytes));
1256                    }
1257                    orig_index += 1;
1258                } else {
1259                    updated_index += 1;
1260                }
1261            }
1262
1263            Ok(final_bytes)
1264        }
1265    }
1266
1267    pub fn with_priority(&self, priority: u64) -> Self {
1268        Self {
1269            reader: self.reader.clone(),
1270            root: self.root.clone(),
1271            block_size: self.block_size,
1272            max_iop_size: self.max_iop_size,
1273            base_priority: priority,
1274            bypass_backpressure: self.bypass_backpressure,
1275            extra_stats: self.extra_stats.clone(),
1276        }
1277    }
1278
1279    /// Returns a copy of this scheduler that additionally records the I/O it
1280    /// performs into `stats`, on top of the scheduler's global statistics.
1281    ///
1282    /// This is the mechanism for measuring exact per-scope (e.g. per-query) I/O:
1283    /// attach a recorder here (e.g. via [`IoStats::recorder`]), perform the reads
1284    /// through the returned handle, then read the totals back with
1285    /// [`IoStats::snapshot`].  The returned handle is cheap to create (a few
1286    /// `Arc` clones) and reuses the same underlying reader, so it does not
1287    /// re-open the file.
1288    pub fn with_io_stats(&self, stats: Arc<dyn IoStatsRecorder>) -> Self {
1289        Self {
1290            extra_stats: Some(stats),
1291            ..self.clone()
1292        }
1293    }
1294
1295    /// Returns a copy of this scheduler that bypasses backpressure for all requests.
1296    ///
1297    /// This should be used for indirect I/O (e.g. fetching items after decoding offsets) where
1298    /// blocking on backpressure could cause a deadlock or excessive latency.
1299    pub fn with_bypass_backpressure(&self) -> Self {
1300        Self {
1301            bypass_backpressure: true,
1302            ..self.clone()
1303        }
1304    }
1305
1306    /// Submit a single IOP to the reader
1307    ///
1308    /// If you have multiple IOPS to perform then [`Self::submit_request`] is going
1309    /// to be more efficient.
1310    ///
1311    /// See [`Self::submit_request`] for more information on the priority and backpressure.
1312    pub fn submit_single(
1313        &self,
1314        range: Range<u64>,
1315        priority: u64,
1316    ) -> impl Future<Output = Result<Bytes>> + Send {
1317        self.submit_request(vec![range], priority)
1318            .map_ok(|vec_bytes| vec_bytes.into_iter().next().unwrap())
1319    }
1320
1321    /// Provides access to the underlying reader
1322    ///
1323    /// Do not use this for reading data as it will bypass any I/O scheduling!
1324    /// This is mainly exposed to allow metadata operations (e.g size, block_size,)
1325    /// which either aren't IOPS or we don't throttle
1326    pub fn reader(&self) -> &Arc<dyn Reader> {
1327        &self.reader
1328    }
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333    use std::{collections::VecDeque, time::Duration};
1334
1335    use futures::poll;
1336    use lance_core::utils::tempfile::TempObjFile;
1337    use rand::RngCore;
1338
1339    use object_store::{GetRange, ObjectStore as OSObjectStore, ObjectStoreExt, memory::InMemory};
1340    use tokio::{runtime::Handle, time::timeout};
1341    use url::Url;
1342
1343    use crate::{
1344        object_store::{DEFAULT_DOWNLOAD_RETRY_COUNT, DEFAULT_MAX_IOP_SIZE},
1345        testing::MockObjectStore,
1346    };
1347
1348    use super::*;
1349
1350    fn make_task(priority: u128, bypass_backpressure: bool) -> IoTask {
1351        IoTask {
1352            reader: Arc::new(TrackingReader {
1353                get_range_count: Arc::new(AtomicU64::new(0)),
1354                path: Path::parse("test").unwrap(),
1355            }),
1356            to_read: 0..1,
1357            when_done: Box::new(|_| {}),
1358            priority,
1359            bypass_backpressure,
1360        }
1361    }
1362
1363    #[test]
1364    fn test_scheduler_state_event_fields() {
1365        use tracing_mock::{expect, subscriber};
1366
1367        let event = expect::event()
1368            .with_target(SCHEDULER_STATE_EVENT_TARGET)
1369            .at_level(tracing::Level::TRACE)
1370            .with_fields(
1371                expect::field("queue_kind")
1372                    .with_value(&"standard")
1373                    .and(expect::field("scheduler_iops").with_value(&7u64))
1374                    .and(expect::field("scheduler_requests").with_value(&3u64))
1375                    .and(expect::field("scheduler_bytes_read").with_value(&4096u64))
1376                    .and(expect::field("io_capacity").with_value(&4u64))
1377                    .and(expect::field("pending_iops").with_value(&1u64))
1378                    .and(expect::field("bytes_available").with_value(&128i64))
1379                    .and(expect::field("head_task_bytes_present").with_value(&true))
1380                    .and(expect::field("head_task_bytes").with_value(&1u64))
1381                    .and(expect::field("head_task_can_deliver_present").with_value(&true))
1382                    .and(expect::field("head_task_can_deliver").with_value(&true)),
1383            );
1384        let (subscriber, handle) = subscriber::mock().event(event).run_with_handle();
1385
1386        let stats = IoStats::new();
1387        stats.add_scan_stats(&ScanStats {
1388            iops: 7,
1389            requests: 3,
1390            bytes_read: 4096,
1391        });
1392        let mut state = IoQueueState::new(4, 192);
1393        state.iops_avail = 2;
1394        state.bytes_avail = 128;
1395        state.pending_requests.push(make_task(1, false));
1396
1397        tracing::subscriber::with_default(subscriber, || {
1398            emit_scheduler_state_event(state.scheduler_state_event(), &stats);
1399        });
1400
1401        handle.assert_finished();
1402    }
1403
1404    #[test]
1405    fn test_iotask_ordering() {
1406        // Bypass tasks must come out of the heap before non-bypass tasks.
1407        // Within each group, lower priority number (= higher priority) comes first.
1408        let mut heap = BinaryHeap::new();
1409        heap.push(make_task(10, false)); // non-bypass, low priority
1410        heap.push(make_task(1, false)); // non-bypass, high priority
1411        heap.push(make_task(20, true)); // bypass, low priority
1412        heap.push(make_task(5, true)); // bypass, high priority
1413
1414        let order: Vec<(u128, bool)> = std::iter::from_fn(|| heap.pop())
1415            .map(|t| (t.priority, t.bypass_backpressure))
1416            .collect();
1417
1418        assert_eq!(order, vec![(5, true), (20, true), (1, false), (10, false)]);
1419    }
1420
1421    #[tokio::test]
1422    async fn test_full_seq_read() {
1423        let tmp_file = TempObjFile::default();
1424
1425        let obj_store = Arc::new(ObjectStore::local());
1426
1427        // Write 1MiB of data
1428        const DATA_SIZE: u64 = 1024 * 1024;
1429        let mut some_data = vec![0; DATA_SIZE as usize];
1430        rand::rng().fill_bytes(&mut some_data);
1431        obj_store.put(&tmp_file, &some_data).await.unwrap();
1432
1433        let config = SchedulerConfig::default_for_testing();
1434
1435        let scheduler = ScanScheduler::new(obj_store, config);
1436
1437        let file_scheduler = scheduler
1438            .open_file(&tmp_file, &CachedFileSize::unknown())
1439            .await
1440            .unwrap();
1441
1442        // Read it back 4KiB at a time
1443        const READ_SIZE: u64 = 4 * 1024;
1444        let mut reqs = VecDeque::new();
1445        let mut offset = 0;
1446        while offset < DATA_SIZE {
1447            reqs.push_back(
1448                #[allow(clippy::single_range_in_vec_init)]
1449                file_scheduler
1450                    .submit_request(vec![offset..offset + READ_SIZE], 0)
1451                    .await
1452                    .unwrap(),
1453            );
1454            offset += READ_SIZE;
1455        }
1456
1457        offset = 0;
1458        // Note: we should get parallel I/O even though we are consuming serially
1459        while offset < DATA_SIZE {
1460            let data = reqs.pop_front().unwrap();
1461            let actual = &data[0];
1462            let expected = &some_data[offset as usize..(offset + READ_SIZE) as usize];
1463            assert_eq!(expected, actual);
1464            offset += READ_SIZE;
1465        }
1466    }
1467
1468    #[tokio::test]
1469    async fn test_open_reader_bridge() {
1470        let tmp_file = TempObjFile::default();
1471
1472        let obj_store = Arc::new(ObjectStore::local());
1473
1474        const DATA_SIZE: u64 = 64 * 1024;
1475        let mut some_data = vec![0; DATA_SIZE as usize];
1476        rand::rng().fill_bytes(&mut some_data);
1477        obj_store.put(&tmp_file, &some_data).await.unwrap();
1478
1479        let config = SchedulerConfig::default_for_testing();
1480        let scheduler = ScanScheduler::new(obj_store.clone(), config);
1481
1482        // Open a bare Reader ourselves, then bridge it into a FileScheduler.
1483        let reader: Arc<dyn Reader> = obj_store.open(&tmp_file).await.unwrap().into();
1484        let file_scheduler = scheduler.open_reader(reader);
1485
1486        let bytes = file_scheduler
1487            .submit_request(vec![0..DATA_SIZE], 0)
1488            .await
1489            .unwrap();
1490        assert_eq!(bytes[0], some_data);
1491    }
1492
1493    #[tokio::test]
1494    async fn test_split_coalesce() {
1495        let tmp_file = TempObjFile::default();
1496
1497        let obj_store = Arc::new(ObjectStore::local());
1498
1499        // Write 75MiB of data
1500        const DATA_SIZE: u64 = 75 * 1024 * 1024;
1501        let mut some_data = vec![0; DATA_SIZE as usize];
1502        rand::rng().fill_bytes(&mut some_data);
1503        obj_store.put(&tmp_file, &some_data).await.unwrap();
1504
1505        let config = SchedulerConfig::default_for_testing();
1506
1507        let scheduler = ScanScheduler::new(obj_store, config);
1508
1509        let file_scheduler = scheduler
1510            .open_file(&tmp_file, &CachedFileSize::unknown())
1511            .await
1512            .unwrap();
1513
1514        // These 3 requests should be coalesced into a single I/O because they are within 4KiB
1515        // of each other
1516        let req =
1517            file_scheduler.submit_request(vec![50_000..51_000, 52_000..53_000, 54_000..55_000], 0);
1518
1519        let bytes = req.await.unwrap();
1520
1521        assert_eq!(bytes[0], &some_data[50_000..51_000]);
1522        assert_eq!(bytes[1], &some_data[52_000..53_000]);
1523        assert_eq!(bytes[2], &some_data[54_000..55_000]);
1524
1525        assert_eq!(1, scheduler.stats().iops);
1526
1527        // This should be split into 5 requests because it is so large
1528        let req = file_scheduler.submit_request(vec![0..DATA_SIZE], 0);
1529        let bytes = req.await.unwrap();
1530        assert!(bytes[0] == some_data, "data is not the same");
1531
1532        assert_eq!(6, scheduler.stats().iops);
1533
1534        // None of these requests are bigger than the max IOP size but they will be coalesced into
1535        // one IOP that is bigger and then split back into 2 requests that don't quite align with the original
1536        // ranges.
1537        let chunk_size = *DEFAULT_MAX_IOP_SIZE;
1538        let req = file_scheduler.submit_request(
1539            vec![
1540                10..chunk_size,
1541                chunk_size + 10..(chunk_size * 2) - 20,
1542                chunk_size * 2..(chunk_size * 2) + 10,
1543            ],
1544            0,
1545        );
1546
1547        let bytes = req.await.unwrap();
1548        let chunk_size = chunk_size as usize;
1549        assert!(
1550            bytes[0] == some_data[10..chunk_size],
1551            "data is not the same"
1552        );
1553        assert!(
1554            bytes[1] == some_data[chunk_size + 10..(chunk_size * 2) - 20],
1555            "data is not the same"
1556        );
1557        assert!(
1558            bytes[2] == some_data[chunk_size * 2..(chunk_size * 2) + 10],
1559            "data is not the same"
1560        );
1561        assert_eq!(8, scheduler.stats().iops);
1562
1563        let reads = (0..44)
1564            .map(|i| i * 1_000_000..(i + 1) * 1_000_000)
1565            .collect::<Vec<_>>();
1566        let req = file_scheduler.submit_request(reads, 0);
1567        let bytes = req.await.unwrap();
1568        for (i, bytes) in bytes.iter().enumerate() {
1569            assert!(
1570                bytes == &some_data[i * 1_000_000..(i + 1) * 1_000_000],
1571                "data is not the same"
1572            );
1573        }
1574        assert_eq!(11, scheduler.stats().iops);
1575    }
1576
1577    #[tokio::test]
1578    async fn test_io_stats_sink() {
1579        let tmp_file = TempObjFile::default();
1580        let obj_store = Arc::new(ObjectStore::local());
1581
1582        const DATA_SIZE: u64 = 1024 * 1024;
1583        let mut some_data = vec![0; DATA_SIZE as usize];
1584        rand::rng().fill_bytes(&mut some_data);
1585        obj_store.put(&tmp_file, &some_data).await.unwrap();
1586
1587        let scheduler = ScanScheduler::new(obj_store, SchedulerConfig::default_for_testing());
1588
1589        // Attach a per-scope sink to one file handle.
1590        let sink = IoStats::new();
1591        let file_scheduler = scheduler
1592            .open_file(&tmp_file, &CachedFileSize::unknown())
1593            .await
1594            .unwrap()
1595            .with_io_stats(sink.recorder());
1596
1597        // Three reads within 4KiB coalesce into a single physical IOP.  The sink
1598        // and the scheduler's global totals must agree exactly, because both are
1599        // recorded from the same post-coalescing request.
1600        file_scheduler
1601            .submit_request(vec![50_000..51_000, 52_000..53_000, 54_000..55_000], 0)
1602            .await
1603            .unwrap();
1604
1605        let global = scheduler.stats();
1606        let scoped = sink.snapshot();
1607        assert_eq!(1, scoped.iops);
1608        assert_eq!(1, scoped.requests);
1609        // Coalesced range 50_000..55_000 => 5000 physical bytes.
1610        assert_eq!(5000, scoped.bytes_read);
1611        assert_eq!(global.iops, scoped.iops);
1612        assert_eq!(global.requests, scoped.requests);
1613        assert_eq!(global.bytes_read, scoped.bytes_read);
1614
1615        // A sibling handle without the sink: the global totals advance but the
1616        // sink stays put, proving per-scope isolation.
1617        let other = scheduler
1618            .open_file(&tmp_file, &CachedFileSize::unknown())
1619            .await
1620            .unwrap();
1621        other.submit_request(vec![0..1000], 0).await.unwrap();
1622
1623        let global_after = scheduler.stats();
1624        let scoped_after = sink.snapshot();
1625        assert_eq!(global.bytes_read + 1000, global_after.bytes_read);
1626        assert_eq!(scoped.bytes_read, scoped_after.bytes_read);
1627        assert_eq!(scoped.iops, scoped_after.iops);
1628    }
1629
1630    #[tokio::test]
1631    async fn test_priority() {
1632        let some_path = Path::parse("foo").unwrap();
1633        let base_store = Arc::new(InMemory::new());
1634        base_store
1635            .put(&some_path, vec![0; 1000].into())
1636            .await
1637            .unwrap();
1638
1639        let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
1640        let mut obj_store = MockObjectStore::default();
1641        let semaphore_copy = semaphore.clone();
1642        obj_store
1643            .expect_get_opts()
1644            .returning(move |location, options| {
1645                let semaphore = semaphore.clone();
1646                let base_store = base_store.clone();
1647                let location = location.clone();
1648                async move {
1649                    semaphore.acquire().await.unwrap().forget();
1650                    base_store.get_opts(&location, options).await
1651                }
1652                .boxed()
1653            });
1654        let obj_store = Arc::new(ObjectStore::new(
1655            Arc::new(obj_store),
1656            Url::parse("mem://").unwrap(),
1657            Some(500),
1658            None,
1659            false,
1660            false,
1661            1,
1662            DEFAULT_DOWNLOAD_RETRY_COUNT,
1663            None,
1664        ));
1665
1666        let config = SchedulerConfig {
1667            io_buffer_size_bytes: 1024 * 1024,
1668            use_lite_scheduler: None,
1669        };
1670
1671        let scan_scheduler = ScanScheduler::new(obj_store, config);
1672
1673        let file_scheduler = scan_scheduler
1674            .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
1675            .await
1676            .unwrap();
1677
1678        // Issue a request, priority doesn't matter, it will be submitted
1679        // immediately (it will go pending)
1680        // Note: the timeout is to prevent a deadlock if the test fails.
1681        let first_fut = timeout(
1682            Duration::from_secs(10),
1683            file_scheduler.submit_single(0..10, 0),
1684        )
1685        .boxed();
1686
1687        // Issue another low priority request (it will go in queue)
1688        let mut second_fut = timeout(
1689            Duration::from_secs(10),
1690            file_scheduler.submit_single(0..20, 100),
1691        )
1692        .boxed();
1693
1694        // Issue a high priority request (it will go in queue and should bump
1695        // the other queued request down)
1696        let mut third_fut = timeout(
1697            Duration::from_secs(10),
1698            file_scheduler.submit_single(0..30, 0),
1699        )
1700        .boxed();
1701
1702        // Finish one file, should be the in-flight first request
1703        semaphore_copy.add_permits(1);
1704        assert!(first_fut.await.unwrap().unwrap().len() == 10);
1705        // Other requests should not be finished
1706        assert!(poll!(&mut second_fut).is_pending());
1707        assert!(poll!(&mut third_fut).is_pending());
1708
1709        // Next should be high priority request
1710        semaphore_copy.add_permits(1);
1711        assert!(third_fut.await.unwrap().unwrap().len() == 30);
1712        assert!(poll!(&mut second_fut).is_pending());
1713
1714        // Finally, the low priority request
1715        semaphore_copy.add_permits(1);
1716        assert!(second_fut.await.unwrap().unwrap().len() == 20);
1717    }
1718
1719    #[tokio::test]
1720    async fn test_standard_scheduler_state_tracks_queue_state() {
1721        let some_path = Path::parse("foo").unwrap();
1722        let base_store = Arc::new(InMemory::new());
1723        base_store
1724            .put(&some_path, vec![0; 1000].into())
1725            .await
1726            .unwrap();
1727
1728        let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
1729        let mut obj_store = MockObjectStore::default();
1730        let semaphore_copy = semaphore.clone();
1731        obj_store
1732            .expect_get_opts()
1733            .returning(move |location, options| {
1734                let semaphore = semaphore.clone();
1735                let base_store = base_store.clone();
1736                let location = location.clone();
1737                async move {
1738                    semaphore.acquire().await.unwrap().forget();
1739                    base_store.get_opts(&location, options).await
1740                }
1741                .boxed()
1742            });
1743        let obj_store = Arc::new(ObjectStore::new(
1744            Arc::new(obj_store),
1745            Url::parse("mem://").unwrap(),
1746            Some(500),
1747            None,
1748            false,
1749            false,
1750            1,
1751            DEFAULT_DOWNLOAD_RETRY_COUNT,
1752            None,
1753        ));
1754
1755        let scheduler = ScanScheduler::new(
1756            obj_store,
1757            SchedulerConfig {
1758                io_buffer_size_bytes: 1024 * 1024,
1759                use_lite_scheduler: Some(false),
1760            },
1761        );
1762        let file_scheduler = scheduler
1763            .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
1764            .await
1765            .unwrap();
1766
1767        let first_fut = timeout(
1768            Duration::from_secs(10),
1769            file_scheduler.submit_single(0..10, 0),
1770        )
1771        .boxed();
1772        let second_fut = timeout(
1773            Duration::from_secs(10),
1774            file_scheduler.submit_single(0..20, 100),
1775        )
1776        .boxed();
1777        let third_fut = timeout(
1778            Duration::from_secs(10),
1779            file_scheduler.submit_single(0..30, 0),
1780        )
1781        .boxed();
1782
1783        let io_queue = match &scheduler.io_queue {
1784            IoQueueType::Standard(io_queue) => io_queue.clone(),
1785            IoQueueType::Lite(_) => unreachable!("test forces the standard scheduler"),
1786        };
1787        let (
1788            io_capacity,
1789            iops_available,
1790            pending_bytes,
1791            bytes_reserved,
1792            priorities_in_flight,
1793            head_task_bytes,
1794            head_task_blocked_by_iops,
1795            head_task_blocked_by_bytes,
1796        ) = timeout(Duration::from_secs(5), async {
1797            loop {
1798                let observed = {
1799                    let state = io_queue.state.lock().unwrap();
1800                    let active_iops = state.io_capacity.saturating_sub(state.iops_avail);
1801                    if active_iops == 1 && state.pending_requests.len() == 2 {
1802                        let pending_bytes = state
1803                            .pending_requests
1804                            .iter()
1805                            .map(IoTask::num_bytes)
1806                            .sum::<u64>();
1807                        let head_task = state.pending_requests.peek().unwrap();
1808                        let bypasses_bytes = state.no_backpressure
1809                            || head_task.bypass_backpressure
1810                            || head_task.priority <= state.priorities_in_flight.min_in_flight();
1811                        Some((
1812                            state.io_capacity,
1813                            state.iops_avail,
1814                            pending_bytes,
1815                            state.io_buffer_size as i64 - state.bytes_avail,
1816                            state.priorities_in_flight.len(),
1817                            head_task.num_bytes(),
1818                            state.iops_avail == 0,
1819                            !bypasses_bytes && head_task.num_bytes() as i64 > state.bytes_avail,
1820                        ))
1821                    } else {
1822                        None
1823                    }
1824                };
1825                if let Some(observed) = observed {
1826                    break observed;
1827                }
1828                tokio::task::yield_now().await;
1829            }
1830        })
1831        .await
1832        .unwrap();
1833
1834        assert_eq!(io_capacity, 1);
1835        assert_eq!(iops_available, 0);
1836        assert_eq!(pending_bytes, 50);
1837        assert_eq!(bytes_reserved, 10);
1838        assert_eq!(priorities_in_flight, 1);
1839        assert_eq!(head_task_bytes, 30);
1840        assert!(head_task_blocked_by_iops);
1841        assert!(!head_task_blocked_by_bytes);
1842
1843        semaphore_copy.add_permits(3);
1844        assert_eq!(first_fut.await.unwrap().unwrap().len(), 10);
1845        assert_eq!(third_fut.await.unwrap().unwrap().len(), 30);
1846        assert_eq!(second_fut.await.unwrap().unwrap().len(), 20);
1847    }
1848
1849    #[tokio::test(flavor = "multi_thread")]
1850    async fn test_backpressure() {
1851        let some_path = Path::parse("foo").unwrap();
1852        let base_store = Arc::new(InMemory::new());
1853        base_store
1854            .put(&some_path, vec![0; 100000].into())
1855            .await
1856            .unwrap();
1857
1858        let bytes_read = Arc::new(AtomicU64::from(0));
1859        let mut obj_store = MockObjectStore::default();
1860        let bytes_read_copy = bytes_read.clone();
1861        // Wraps the obj_store to keep track of how many bytes have been read
1862        obj_store
1863            .expect_get_opts()
1864            .returning(move |location, options| {
1865                let range = options.range.as_ref().unwrap();
1866                let num_bytes = match range {
1867                    GetRange::Bounded(bounded) => bounded.end - bounded.start,
1868                    _ => panic!(),
1869                };
1870                bytes_read_copy.fetch_add(num_bytes, Ordering::Release);
1871                let location = location.clone();
1872                let base_store = base_store.clone();
1873                async move { base_store.get_opts(&location, options).await }.boxed()
1874            });
1875        let obj_store = Arc::new(ObjectStore::new(
1876            Arc::new(obj_store),
1877            Url::parse("mem://").unwrap(),
1878            Some(500),
1879            None,
1880            false,
1881            false,
1882            1,
1883            DEFAULT_DOWNLOAD_RETRY_COUNT,
1884            None,
1885        ));
1886
1887        let config = SchedulerConfig {
1888            io_buffer_size_bytes: 10,
1889            use_lite_scheduler: None,
1890        };
1891
1892        let scan_scheduler = ScanScheduler::new(obj_store.clone(), config);
1893
1894        let file_scheduler = scan_scheduler
1895            .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(100000))
1896            .await
1897            .unwrap();
1898
1899        let wait_for_idle = || async move {
1900            let handle = Handle::current();
1901            while handle.metrics().num_alive_tasks() != 1 {
1902                tokio::time::sleep(Duration::from_millis(10)).await;
1903            }
1904        };
1905        let wait_for_bytes_read_and_idle = |target_bytes: u64| {
1906            // We need to move `target` but don't want to move `bytes_read`
1907            let bytes_read = &bytes_read;
1908            async move {
1909                let bytes_read_copy = bytes_read.clone();
1910                while bytes_read_copy.load(Ordering::Acquire) < target_bytes {
1911                    tokio::time::sleep(Duration::from_millis(10)).await;
1912                }
1913                wait_for_idle().await;
1914            }
1915        };
1916
1917        // This read will begin immediately
1918        let first_fut = file_scheduler.submit_single(0..5, 0);
1919        // This read should also begin immediately
1920        let second_fut = file_scheduler.submit_single(0..5, 0);
1921        // This read will be throttled
1922        let third_fut = file_scheduler.submit_single(0..3, 0);
1923        // Two tasks (third_fut and unit test)
1924        wait_for_bytes_read_and_idle(10).await;
1925
1926        assert_eq!(first_fut.await.unwrap().len(), 5);
1927        // One task (unit test)
1928        wait_for_bytes_read_and_idle(13).await;
1929
1930        // 2 bytes are ready but 5 bytes requested, read will be blocked
1931        let fourth_fut = file_scheduler.submit_single(0..5, 0);
1932        wait_for_bytes_read_and_idle(13).await;
1933
1934        // Out of order completion is ok, will unblock backpressure
1935        assert_eq!(third_fut.await.unwrap().len(), 3);
1936        wait_for_bytes_read_and_idle(18).await;
1937
1938        assert_eq!(second_fut.await.unwrap().len(), 5);
1939        // At this point there are 5 bytes available in backpressure queue
1940        // Now we issue multi-read that can be partially fulfilled, it will read some bytes but
1941        // not all of them. (using large range gap to ensure request not coalesced)
1942        //
1943        // I'm actually not sure this behavior is great.  It's possible that we should just
1944        // block until we can fulfill the entire request.
1945        let fifth_fut = file_scheduler.submit_request(vec![0..3, 90000..90007], 0);
1946        wait_for_bytes_read_and_idle(21).await;
1947
1948        // Fifth future should eventually finish due to deadlock prevention
1949        let fifth_bytes = tokio::time::timeout(Duration::from_secs(10), fifth_fut)
1950            .await
1951            .unwrap();
1952        assert_eq!(
1953            fifth_bytes.unwrap().iter().map(|b| b.len()).sum::<usize>(),
1954            10
1955        );
1956
1957        // And now let's just make sure that we can read the rest of the data
1958        assert_eq!(fourth_fut.await.unwrap().len(), 5);
1959        wait_for_bytes_read_and_idle(28).await;
1960
1961        // Ensure deadlock prevention timeout can be disabled
1962        let config = SchedulerConfig {
1963            io_buffer_size_bytes: 10,
1964            use_lite_scheduler: None,
1965        };
1966
1967        let scan_scheduler = ScanScheduler::new(obj_store, config);
1968        let file_scheduler = scan_scheduler
1969            .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(100000))
1970            .await
1971            .unwrap();
1972
1973        let first_fut = file_scheduler.submit_single(0..10, 0);
1974        let second_fut = file_scheduler.submit_single(0..10, 0);
1975
1976        std::thread::sleep(Duration::from_millis(100));
1977        assert_eq!(first_fut.await.unwrap().len(), 10);
1978        assert_eq!(second_fut.await.unwrap().len(), 10);
1979    }
1980
1981    #[derive(Debug)]
1982    struct BlockingReader {
1983        semaphore: Arc<tokio::sync::Semaphore>,
1984        get_range_count: Arc<AtomicU64>,
1985        path: Path,
1986    }
1987
1988    impl lance_core::deepsize::DeepSizeOf for BlockingReader {
1989        fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
1990            0
1991        }
1992    }
1993
1994    impl Reader for BlockingReader {
1995        fn path(&self) -> &Path {
1996            &self.path
1997        }
1998
1999        fn block_size(&self) -> usize {
2000            4096
2001        }
2002
2003        fn io_parallelism(&self) -> usize {
2004            1
2005        }
2006
2007        fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
2008            Box::pin(async { Ok(1_000_000) })
2009        }
2010
2011        fn get_range(
2012            &self,
2013            range: Range<usize>,
2014        ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
2015            self.get_range_count.fetch_add(1, Ordering::Release);
2016            let semaphore = self.semaphore.clone();
2017            let num_bytes = range.end - range.start;
2018            Box::pin(async move {
2019                semaphore.acquire().await.unwrap().forget();
2020                Ok(Bytes::from(vec![0u8; num_bytes]))
2021            })
2022        }
2023
2024        fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
2025            Box::pin(async { Ok(Bytes::from(vec![0u8; 1_000_000])) })
2026        }
2027    }
2028
2029    #[tokio::test(flavor = "multi_thread")]
2030    async fn test_same_priority_chunks_continue_after_higher_priority_request() {
2031        let obj_store = Arc::new(ObjectStore::new(
2032            Arc::new(InMemory::new()),
2033            Url::parse("mem://").unwrap(),
2034            Some(4096),
2035            None,
2036            false,
2037            false,
2038            1,
2039            DEFAULT_DOWNLOAD_RETRY_COUNT,
2040            None,
2041        ));
2042        let scheduler = ScanScheduler::new(
2043            obj_store,
2044            SchedulerConfig {
2045                io_buffer_size_bytes: 10,
2046                use_lite_scheduler: Some(false),
2047            },
2048        );
2049        let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2050        let reader: Arc<dyn Reader> = Arc::new(BlockingReader {
2051            semaphore: semaphore.clone(),
2052            get_range_count: Arc::new(AtomicU64::new(0)),
2053            path: Path::parse("test").unwrap(),
2054        });
2055
2056        let low_priority =
2057            scheduler.submit_request(reader.clone(), vec![0..6, 100..106], 10, false);
2058        let high_priority = scheduler.submit_request(reader, vec![200..204], 0, false);
2059
2060        semaphore.add_permits(3);
2061        let low_priority = timeout(Duration::from_secs(5), low_priority)
2062            .await
2063            .unwrap()
2064            .unwrap();
2065        assert_eq!(
2066            low_priority.iter().map(|bytes| bytes.len()).sum::<usize>(),
2067            12
2068        );
2069
2070        let high_priority = timeout(Duration::from_secs(5), high_priority)
2071            .await
2072            .unwrap()
2073            .unwrap();
2074        assert_eq!(high_priority[0].len(), 4);
2075    }
2076
2077    /// A Reader that tracks how many times get_range has been called.
2078    #[derive(Debug)]
2079    struct TrackingReader {
2080        get_range_count: Arc<AtomicU64>,
2081        path: Path,
2082    }
2083
2084    impl lance_core::deepsize::DeepSizeOf for TrackingReader {
2085        fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
2086            0
2087        }
2088    }
2089
2090    impl Reader for TrackingReader {
2091        fn path(&self) -> &Path {
2092            &self.path
2093        }
2094
2095        fn block_size(&self) -> usize {
2096            4096
2097        }
2098
2099        fn io_parallelism(&self) -> usize {
2100            1
2101        }
2102
2103        fn size(&self) -> futures::future::BoxFuture<'_, object_store::Result<usize>> {
2104            Box::pin(async { Ok(1_000_000) })
2105        }
2106
2107        fn get_range(
2108            &self,
2109            range: Range<usize>,
2110        ) -> futures::future::BoxFuture<'static, object_store::Result<Bytes>> {
2111            self.get_range_count.fetch_add(1, Ordering::Release);
2112            let num_bytes = range.end - range.start;
2113            Box::pin(async move { Ok(Bytes::from(vec![0u8; num_bytes])) })
2114        }
2115
2116        fn get_all(&self) -> futures::future::BoxFuture<'_, object_store::Result<Bytes>> {
2117            Box::pin(async { Ok(Bytes::from(vec![0u8; 1_000_000])) })
2118        }
2119    }
2120
2121    #[tokio::test]
2122    async fn test_lite_scheduler_submits_eagerly() {
2123        let obj_store = Arc::new(ObjectStore::memory());
2124        let config = SchedulerConfig::default_for_testing().with_lite_scheduler();
2125        let scheduler = ScanScheduler::new(obj_store, config);
2126
2127        let get_range_count = Arc::new(AtomicU64::new(0));
2128        let reader: Arc<dyn Reader> = Arc::new(TrackingReader {
2129            get_range_count: get_range_count.clone(),
2130            path: Path::parse("test").unwrap(),
2131        });
2132
2133        // Submit several requests. The lite scheduler should call get_range
2134        // eagerly during submit (before the returned future is polled).
2135        let fut1 = scheduler.submit_request(reader.clone(), vec![0..100], 0, false);
2136        let fut2 = scheduler.submit_request(reader.clone(), vec![100..200], 10, false);
2137        let fut3 = scheduler.submit_request(reader.clone(), vec![200..300], 20, false);
2138
2139        // get_range must have been called for all 3 requests already.
2140        assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2141
2142        // The futures should still resolve with the correct data.
2143        assert_eq!(fut1.await.unwrap()[0].len(), 100);
2144        assert_eq!(fut2.await.unwrap()[0].len(), 100);
2145        assert_eq!(fut3.await.unwrap()[0].len(), 100);
2146    }
2147
2148    #[tokio::test]
2149    async fn test_object_store_selects_scheduler() {
2150        // A memory:// store should use the standard scheduler when config is None
2151        let memory_store = Arc::new(ObjectStore::memory());
2152        assert!(!memory_store.prefers_lite_scheduler());
2153        let config = SchedulerConfig {
2154            io_buffer_size_bytes: 256 * 1024 * 1024,
2155            use_lite_scheduler: None,
2156        };
2157        let scheduler = ScanScheduler::new(memory_store.clone(), config);
2158        assert!(!scheduler.uses_lite_scheduler());
2159
2160        // A file+uring:// store should use the lite scheduler when config is None
2161        let uring_store = Arc::new(ObjectStore::new(
2162            Arc::new(InMemory::new()),
2163            Url::parse("file+uring:///tmp").unwrap(),
2164            None,
2165            None,
2166            false,
2167            false,
2168            8,
2169            DEFAULT_DOWNLOAD_RETRY_COUNT,
2170            None,
2171        ));
2172        assert!(uring_store.prefers_lite_scheduler());
2173        let config = SchedulerConfig {
2174            io_buffer_size_bytes: 256 * 1024 * 1024,
2175            use_lite_scheduler: None,
2176        };
2177        let scheduler = ScanScheduler::new(uring_store.clone(), config);
2178        assert!(scheduler.uses_lite_scheduler());
2179
2180        // Explicit Some(false) overrides a file+uring:// store's preference
2181        let config = SchedulerConfig {
2182            io_buffer_size_bytes: 256 * 1024 * 1024,
2183            use_lite_scheduler: Some(false),
2184        };
2185        let scheduler = ScanScheduler::new(uring_store, config);
2186        assert!(!scheduler.uses_lite_scheduler());
2187
2188        // Explicit Some(true) overrides a memory:// store's preference
2189        let config = SchedulerConfig {
2190            io_buffer_size_bytes: 256 * 1024 * 1024,
2191            use_lite_scheduler: Some(true),
2192        };
2193        let scheduler = ScanScheduler::new(memory_store, config);
2194        assert!(scheduler.uses_lite_scheduler());
2195    }
2196
2197    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2198    async fn stress_backpressure() {
2199        // This test ensures that the backpressure mechanism works correctly with
2200        // regards to priority.  In other words, as long as all requests are consumed
2201        // in priority order then the backpressure mechanism should not deadlock
2202        let some_path = Path::parse("foo").unwrap();
2203        let obj_store = Arc::new(ObjectStore::memory());
2204        obj_store
2205            .put(&some_path, vec![0; 100000].as_slice())
2206            .await
2207            .unwrap();
2208
2209        // Only one request will be allowed in
2210        let config = SchedulerConfig {
2211            io_buffer_size_bytes: 1,
2212            use_lite_scheduler: None,
2213        };
2214        let scan_scheduler = ScanScheduler::new(obj_store.clone(), config);
2215        let file_scheduler = scan_scheduler
2216            .open_file(&some_path, &CachedFileSize::unknown())
2217            .await
2218            .unwrap();
2219
2220        let mut futs = Vec::with_capacity(10000);
2221        for idx in 0..10000 {
2222            futs.push(file_scheduler.submit_single(idx..idx + 1, idx));
2223        }
2224
2225        for fut in futs {
2226            fut.await.unwrap();
2227        }
2228    }
2229
2230    #[tokio::test(flavor = "multi_thread")]
2231    async fn test_zero_buffer_size_no_backpressure() {
2232        // With io_buffer_size_bytes=0 (no_backpressure=true), reads at any priority go
2233        // through without blocking, even though a zero budget would normally halt all I/O.
2234        let obj_store = Arc::new(ObjectStore::memory());
2235        let config = SchedulerConfig {
2236            io_buffer_size_bytes: 0,
2237            use_lite_scheduler: Some(false),
2238        };
2239        let scheduler = ScanScheduler::new(obj_store, config);
2240
2241        let get_range_count = Arc::new(AtomicU64::new(0));
2242        let reader: Arc<dyn Reader> = Arc::new(TrackingReader {
2243            get_range_count: get_range_count.clone(),
2244            path: Path::parse("test").unwrap(),
2245        });
2246
2247        // Submit three reads at increasing priorities without awaiting any first.
2248        // Priority 1 and 2 would deadlock under a real 0-byte budget without no_backpressure.
2249        let fut1 = scheduler.submit_request(reader.clone(), vec![0..1000], 0, false);
2250        let fut2 = scheduler.submit_request(reader.clone(), vec![1000..2000], 1, false);
2251        let fut3 = scheduler.submit_request(reader.clone(), vec![2000..3000], 2, false);
2252
2253        let bytes1 = timeout(Duration::from_secs(5), fut1)
2254            .await
2255            .unwrap()
2256            .unwrap();
2257        let bytes2 = timeout(Duration::from_secs(5), fut2)
2258            .await
2259            .unwrap()
2260            .unwrap();
2261        let bytes3 = timeout(Duration::from_secs(5), fut3)
2262            .await
2263            .unwrap()
2264            .unwrap();
2265        assert_eq!(bytes1[0].len(), 1000);
2266        assert_eq!(bytes2[0].len(), 1000);
2267        assert_eq!(bytes3[0].len(), 1000);
2268        assert_eq!(get_range_count.load(Ordering::Acquire), 3);
2269    }
2270
2271    #[tokio::test(flavor = "multi_thread")]
2272    async fn test_file_scheduler_bypass_backpressure() {
2273        // A FileScheduler obtained via with_bypass_backpressure() submits reads that bypass
2274        // the byte budget, allowing them to proceed even when the budget is exhausted.
2275        let some_path = Path::parse("foo").unwrap();
2276        let base_store = Arc::new(InMemory::new());
2277        base_store
2278            .put(&some_path, vec![0u8; 1000].into())
2279            .await
2280            .unwrap();
2281
2282        let bytes_dispatched = Arc::new(AtomicU64::from(0));
2283        let mut obj_store = MockObjectStore::default();
2284        let bytes_dispatched_copy = bytes_dispatched.clone();
2285        obj_store
2286            .expect_get_opts()
2287            .returning(move |location, options| {
2288                let range = options.range.as_ref().unwrap();
2289                let num_bytes = match range {
2290                    GetRange::Bounded(bounded) => bounded.end - bounded.start,
2291                    _ => panic!(),
2292                };
2293                bytes_dispatched_copy.fetch_add(num_bytes, Ordering::Release);
2294                let location = location.clone();
2295                let base_store = base_store.clone();
2296                async move { base_store.get_opts(&location, options).await }.boxed()
2297            });
2298        let obj_store = Arc::new(ObjectStore::new(
2299            Arc::new(obj_store),
2300            Url::parse("mem://").unwrap(),
2301            Some(500),
2302            None,
2303            false,
2304            false,
2305            1,
2306            DEFAULT_DOWNLOAD_RETRY_COUNT,
2307            None,
2308        ));
2309
2310        // Budget = 10 bytes.
2311        let config = SchedulerConfig {
2312            io_buffer_size_bytes: 10,
2313            use_lite_scheduler: Some(false),
2314        };
2315        let scan_scheduler = ScanScheduler::new(obj_store, config);
2316        let file_scheduler = scan_scheduler
2317            .open_file(&Path::parse("foo").unwrap(), &CachedFileSize::new(1000))
2318            .await
2319            .unwrap();
2320        let bypass_scheduler = file_scheduler.with_bypass_backpressure();
2321
2322        // Fill the 10-byte budget with a priority-0 read.
2323        let blocker_fut = file_scheduler.submit_single(0..10, 0);
2324        while bytes_dispatched.load(Ordering::Acquire) < 10 {
2325            tokio::time::sleep(Duration::from_millis(1)).await;
2326        }
2327
2328        // A normal read at priority 2 is blocked: budget = 0, priority 2 > min-in-flight 0.
2329        // A bypass read at priority 1 (higher priority in the queue) bypasses the budget check.
2330        let normal_fut = file_scheduler.submit_single(0..10, 2);
2331        let bypass_fut = bypass_scheduler.submit_single(0..10, 1);
2332
2333        // Bypass read is dispatched; normal read is still blocked.
2334        while bytes_dispatched.load(Ordering::Acquire) < 20 {
2335            tokio::time::sleep(Duration::from_millis(1)).await;
2336        }
2337        tokio::time::sleep(Duration::from_millis(20)).await;
2338        assert_eq!(
2339            bytes_dispatched.load(Ordering::Acquire),
2340            20,
2341            "normal read should still be blocked while budget is exhausted"
2342        );
2343
2344        // Consuming the blocker releases its 10-byte budget → normal read can proceed.
2345        timeout(Duration::from_secs(5), blocker_fut)
2346            .await
2347            .unwrap()
2348            .unwrap();
2349        timeout(Duration::from_secs(5), bypass_fut)
2350            .await
2351            .unwrap()
2352            .unwrap();
2353        timeout(Duration::from_secs(5), normal_fut)
2354            .await
2355            .unwrap()
2356            .unwrap();
2357        assert_eq!(bytes_dispatched.load(Ordering::Acquire), 30);
2358    }
2359
2360    // Against a 100-byte budget: submit fut1 (50 bytes, priority 0), drop it while
2361    // its read is still blocked in get_range, then submit fut2 (60 bytes, priority 1).
2362    // fut2's priority can't win the priority-bypass, so it needs 60 of the budget --
2363    // available only if fut1's dropped reservation was refunded. Returns whether fut2
2364    // completed within 2s (false = the reservation leaked and fut2 deadlocked).
2365    async fn run_caller_drop_scenario(use_lite_scheduler: bool) -> (bool, Duration) {
2366        let obj_store = Arc::new(ObjectStore::new(
2367            Arc::new(InMemory::new()),
2368            Url::parse("mem://").unwrap(),
2369            Some(4096),
2370            None,
2371            false,
2372            false,
2373            1,
2374            DEFAULT_DOWNLOAD_RETRY_COUNT,
2375            None,
2376        ));
2377        let scheduler = ScanScheduler::new(
2378            obj_store,
2379            SchedulerConfig {
2380                io_buffer_size_bytes: 100,
2381                use_lite_scheduler: Some(use_lite_scheduler),
2382            },
2383        );
2384
2385        let semaphore = Arc::new(tokio::sync::Semaphore::new(0));
2386        let get_range_count = Arc::new(AtomicU64::new(0));
2387        let reader: Arc<dyn Reader> = Arc::new(BlockingReader {
2388            semaphore: semaphore.clone(),
2389            get_range_count: get_range_count.clone(),
2390            path: Path::parse("test").unwrap(),
2391        });
2392
2393        // Step 1: reserve 50 of the 100 budget bytes with a read we never consume.
2394        // Spawn it so we can cancel the caller-side future while it is still parked
2395        // waiting for the (blocked) read to finish.
2396        let fut1 = scheduler.submit_request(reader.clone(), vec![0..50], 0, false);
2397        let handle = tokio::spawn(async move {
2398            let _ = fut1.await;
2399        });
2400
2401        // Wait until the read is genuinely in flight (blocked on the semaphore).
2402        // This guarantees the 50-byte reservation has been taken before we drop
2403        // the caller, closing the race between the I/O loop and the abort.
2404        while get_range_count.load(Ordering::Acquire) == 0 {
2405            tokio::time::sleep(Duration::from_millis(1)).await;
2406        }
2407
2408        // Step 2: drop the caller-side future while its `rx` is still pending.
2409        handle.abort();
2410        let _ = handle.await;
2411
2412        // Step 3: let the in-flight read finish. The reservation should be refunded
2413        // now that the request is done, whether or not the caller is still around.
2414        semaphore.add_permits(1);
2415        // Give the read time to run to completion so the refund would already have
2416        // happened.
2417        tokio::time::sleep(Duration::from_millis(50)).await;
2418
2419        // Step 4: submit the follow-up. Add a permit up front so that, if it *is*
2420        // admitted, its own read can complete rather than block on the semaphore.
2421        semaphore.add_permits(1);
2422        let fut2 = scheduler.submit_request(reader, vec![100..160], 1, false);
2423
2424        let start = std::time::Instant::now();
2425        let outcome = timeout(Duration::from_secs(2), fut2).await;
2426        let elapsed = start.elapsed();
2427        match outcome {
2428            Ok(res) => {
2429                assert_eq!(res.unwrap().iter().map(|b| b.len()).sum::<usize>(), 60);
2430                (true, elapsed)
2431            }
2432            Err(_) => (false, elapsed),
2433        }
2434    }
2435
2436    /// Dropping a standard-scheduler request future while its read is in flight must
2437    /// still refund the backpressure reservation, so a later request that needs the
2438    /// budget does not deadlock.
2439    #[tokio::test(flavor = "multi_thread")]
2440    async fn standard_scheduler_refunds_reservation_on_caller_drop() {
2441        let (completed, elapsed) = run_caller_drop_scenario(false).await;
2442        assert!(
2443            completed,
2444            "standard scheduler deadlocked the follow-up request (elapsed {elapsed:?}); \
2445             the dropped request's reservation was not refunded"
2446        );
2447    }
2448
2449    /// Same guarantee for the lite scheduler: dropping a request future mid-read
2450    /// releases its reservation via the `TaskHandle` drop path.
2451    #[tokio::test(flavor = "multi_thread")]
2452    async fn lite_scheduler_refunds_reservation_on_caller_drop() {
2453        let (completed, elapsed) = run_caller_drop_scenario(true).await;
2454        assert!(
2455            completed,
2456            "lite scheduler deadlocked the follow-up request (elapsed {elapsed:?}); \
2457             the dropped request's reservation was not refunded"
2458        );
2459    }
2460}