vortex_io/request/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! IO requests, results, and the source that fulfils them.
5//!
6//! The protocol moves bytes at a range. A source has exactly one other property a consumer may
7//! ask for, its length, because a Vortex file cannot be located from its start without it.
8//! Nothing else is a request: anything a stage needs that is not bytes at a range is
9//! construction-time data for that stage.
10
11use std::sync::Arc;
12use std::task::Context;
13use std::task::Poll;
14
15use vortex_array::buffer::BufferHandle;
16use vortex_buffer::Alignment;
17use vortex_error::VortexResult;
18use vortex_error::vortex_panic;
19
20mod source;
21
22pub use source::ReadAtIoSource;
23
24/// Consumer-chosen label for one request, unique within the issuing object.
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
26pub struct IoRequestId(pub u32);
27
28/// What to fetch from the source: its length, or bytes at a range. This enum is closed; see the
29/// module documentation.
30#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
31pub enum IoTarget {
32 /// Total length of the source in bytes.
33 Size,
34 /// An absolute byte range.
35 Range {
36 /// Absolute byte offset of the first byte.
37 offset: u64,
38 /// Number of bytes to read.
39 len: usize,
40 /// The alignment the delivered bytes have at least.
41 alignment: Alignment,
42 },
43}
44
45impl IoTarget {
46 /// A byte range whose bytes need no particular alignment.
47 pub fn range(offset: u64, len: usize) -> Self {
48 Self::Range {
49 offset,
50 len,
51 alignment: Alignment::none(),
52 }
53 }
54}
55
56/// A stage's reason for registering bytes. Optional hints may be declined by the service.
57#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
58pub enum IoIntent {
59 /// Required bytes, delivered to the requesting object.
60 Fetch,
61 /// Optional bytes eligible for an independent background read.
62 Prefetch,
63 /// Coalescing interest only; never starts an independent read.
64 Announce,
65}
66
67/// One request: a consumer-chosen id and the target it names.
68#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
69pub struct IoRequest {
70 /// Required delivery, background prefetch, or coalescing-only interest.
71 pub intent: IoIntent,
72 /// Identifies the request within the issuing object.
73 pub request: IoRequestId,
74 /// What to fetch.
75 pub target: IoTarget,
76}
77
78/// Requests an object publishes together, each exactly once.
79pub type IoBatch = Vec<IoRequest>;
80
81/// Result for one request. The variant matches the target: `Size` for `Size`, `Bytes` for
82/// `Range`. The driver checks this before delivering, so a consumer never sees a mismatch.
83#[derive(Debug, Clone)]
84pub enum IoResult {
85 /// Total length of the source in bytes.
86 Size(u64),
87 /// The bytes of a range request, possibly not yet resident on the host.
88 Bytes(BufferHandle),
89}
90
91impl IoResult {
92 /// Whether this is the variant `target` must be answered with.
93 pub fn matches(&self, target: &IoTarget) -> bool {
94 matches!(
95 (self, target),
96 (Self::Size(_), IoTarget::Size) | (Self::Bytes(_), IoTarget::Range { .. })
97 )
98 }
99
100 /// The variant name, for messages.
101 pub fn kind(&self) -> &'static str {
102 match self {
103 Self::Size(_) => "size",
104 Self::Bytes(_) => "bytes",
105 }
106 }
107}
108
109/// Opaque owner identity chosen by the caller, unique within a registration scope.
110#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
111pub struct IoOwnerId(pub u64);
112
113/// One finished request: its owner, its request identity, and the answer.
114pub struct Completion {
115 /// The owner that submitted the request.
116 pub owner: IoOwnerId,
117 /// The request's consumer-chosen id.
118 pub request: IoRequestId,
119 /// The answer, or the read error, which fails the registration scope.
120 pub result: VortexResult<IoResult>,
121}
122
123/// IO registration and dispatch for one registration scope, such as the work descended from one
124/// root planner. Services may decline optional hints. Accepted optional work belongs to the
125/// source, not to the publishing stage, and lives until the source is cleared or dropped.
126pub trait IoSource: Send + Sync {
127 /// Registers a whole batch before any of its reads become eligible for dispatch.
128 /// Registration does not perform IO and never blocks.
129 fn submit(&self, owner: IoOwnerId, batch: IoBatch) -> VortexResult<()>;
130
131 /// Advances eligible IO without blocking and returns a required completion if ready.
132 /// Optional successes stay in the source; any read failure fails the run.
133 fn poll(&self) -> VortexResult<Option<Completion>>;
134
135 /// Like [`poll`](Self::poll), registering `cx` to be woken when a required completion is
136 /// ready. Fails when no fetch is in flight, since nothing would ever wake the caller.
137 fn poll_completion(&self, cx: &mut Context<'_>) -> Poll<VortexResult<Completion>>;
138
139 /// Blocks until a required completion is available or a read fails.
140 fn wait(&self) -> VortexResult<Completion>;
141
142 /// Releases a retired object's fetch subscriptions, preserving optional announcements.
143 fn release(&self, owner: IoOwnerId);
144
145 /// Cancels the scope's registrations and drops retained bytes and pending completions.
146 fn clear(&self);
147}
148
149/// A byte source's IO service, shared by everything that reads the source.
150///
151/// Each registration scope reads through its own [`IoSource`], and the service is free to share
152/// and coalesce the reads of all of them.
153pub trait IoService: Send + Sync {
154 /// A fresh source for one registration scope.
155 fn session(&self) -> Arc<dyn IoSource>;
156}
157
158/// Receives fulfilled requests. Implementations only store; they never compute here.
159pub trait IoConsumer {
160 /// Stores the result for `request`.
161 ///
162 /// The driver only delivers fetches the consumer published and results that match their
163 /// targets, so anything else is a driver bug and the consumer may panic.
164 fn set_io_result(&mut self, request: IoRequestId, result: IoResult);
165}
166
167/// One outstanding request at a time, with ids that never repeat within the owner.
168///
169/// A consumer that issues requests one after another keeps its id allocation, its in-flight
170/// request, and the delivered result here, so its own state carries none of them.
171#[derive(Default)]
172pub struct IoSlot {
173 next_id: u32,
174 request: Option<IoRequest>,
175 result: Option<IoResult>,
176}
177
178impl IoSlot {
179 /// Issues a request for `target` with a fresh id. Panics if one is already outstanding or
180 /// its result has not been taken, since that is a bug in the owner.
181 pub fn issue(&mut self, target: IoTarget) -> IoBatch {
182 if self.request.is_some() || self.result.is_some() {
183 vortex_panic!("IoSlot: issue while a request is outstanding");
184 }
185 let request = IoRequest {
186 intent: IoIntent::Fetch,
187 request: IoRequestId(self.next_id),
188 target,
189 };
190 self.next_id += 1;
191 self.request = Some(request);
192 vec![request]
193 }
194
195 /// Whether the issued request is still undelivered.
196 pub fn is_waiting(&self) -> bool {
197 self.request.is_some()
198 }
199
200 /// Stores the result for the in-flight request. Panics on an unknown id or a result that
201 /// does not match the target, since the driver guarantees both.
202 pub fn deliver(&mut self, id: IoRequestId, result: IoResult) {
203 let Some(request) = self.request.take_if(|request| request.request == id) else {
204 vortex_panic!("IoSlot: delivery of {id:?}, which is not outstanding");
205 };
206 if !result.matches(&request.target) {
207 vortex_panic!(
208 "IoSlot: {id:?} for {:?} answered with {}",
209 request.target,
210 result.kind()
211 );
212 }
213 self.result = Some(result);
214 }
215
216 /// The delivered result, once.
217 pub fn take(&mut self) -> Option<IoResult> {
218 self.result.take()
219 }
220}
221
222#[cfg(test)]
223mod tests;