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