Skip to main content

lance_io/
scheduler.rs

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