This is an automated email from the ASF dual-hosted git repository.
erickguan pushed a commit to branch fix-reader
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/fix-reader by this push:
new 68bcee2dd chore: restore read range RFC wording
68bcee2dd is described below
commit 68bcee2dd256ba6f2df826e4c16c3a6640889ffd
Author: Erick Guan <[email protected]>
AuthorDate: Sun Jul 5 12:26:02 2026 +0800
chore: restore read range RFC wording
---
.../docs/rfcs/7660_move_read_range_to_reader.md | 101 ++++++++++-----------
1 file changed, 46 insertions(+), 55 deletions(-)
diff --git a/core/core/src/docs/rfcs/7660_move_read_range_to_reader.md
b/core/core/src/docs/rfcs/7660_move_read_range_to_reader.md
index 1881e1780..623906f60 100644
--- a/core/core/src/docs/rfcs/7660_move_read_range_to_reader.md
+++ b/core/core/src/docs/rfcs/7660_move_read_range_to_reader.md
@@ -6,19 +6,19 @@
# Summary
Change OpenDAL's raw read contract so byte ranges are selected by the raw
reader
-instead of `Service::read`.
+instead of `Access::read`.
-Today `Service::read(path, OpRead { range, .. })` opens one concrete range
+Today `Access::read(path, OpRead { range, .. })` opens one concrete range
stream. This works well for HTTP object stores, where each range read is an
independent request. It is a poor fit for services such as `fs`, `sftp`,
`hdfs`, `hdfs-native`, and `monoiofs`, where the service may keep an opened
handle and execute multiple cursor-independent range reads against it. The
-current core reader planner still calls `Service::read` for every planned
range,
+current core reader planner still calls `Access::read` for every planned range,
forcing those services to repeatedly open and close handles.
-This RFC keeps the `Service::read` operation, but changes its contract:
+This RFC keeps the `Access::read` operation, but changes its contract:
-- `Service::read(path, OpRead)` creates a reusable raw `oio::Read` for the path
+- `Access::read(path, OpRead)` creates a reusable raw `oio::Read` for the path
and read condition set.
- `oio::Read::open(range)` opens a range-scoped byte stream.
- `oio::Read::read(range)` reads one bounded planner range into one `Buffer`.
@@ -41,12 +41,12 @@ let parts = reader.fetch(vec![0..1024, 4096..8192]).await?;
However, the current implementation plans these public calls by generating new
raw range streams. `ReadGenerator::next_reader` and `ChunkedReader` both call
-`Service::read(path, args.with_range(range))` for every range. For `s3`, `gcs`,
+`Access::read(path, args.with_range(range))` for every range. For `s3`, `gcs`,
and `azblob`, this is natural because each range is a new HTTP request. For
handle based services, this means every range repeats open and seek work even
when the target data can be represented by a reusable handle.
-The root cause is that `Service::read` is modeled as a transport-level range
+The root cause is that `Access::read` is modeled as a transport-level range
request. OpenDAL needs a storage-neutral raw model:
- Object stores can implement a raw reader that issues an independent ranged
@@ -84,7 +84,7 @@ let ranges = reader.fetch(vec![0..1024, 4096..8192]).await?;
The difference is internal. OpenDAL creates one reusable raw reader for
`reader("path/to/file")`. After that, range operations are executed by this raw
-reader instead of reopening through `Service::read` for every range.
+reader instead of reopening through `Access::read` for every range.
For HTTP object stores, the raw reader stores the path and read arguments, then
sends a new ranged request for each range. Behavior and cost stay the same.
@@ -98,18 +98,17 @@ a shared seek cursor. It must use cursor-independent reads
such as `pread` /
## Raw read contract
-`Service::read` remains the raw read operation:
+`Access::read` remains the raw read operation:
```rust,ignore
-pub trait Service: Send + Sync + Debug + Unpin + 'static {
+pub trait Access: Send + Sync + Debug + Unpin + 'static {
type Reader: oio::Read;
fn read(
&self,
- ctx: &OperationContext,
path: &str,
args: OpRead,
- ) -> Result<Self::Reader>;
+ ) -> impl Future<Output = Result<(RpRead, Self::Reader)>> + MaybeSend;
}
```
@@ -118,7 +117,7 @@ The meaning changes:
- `path` and `OpRead` select a path and read condition set.
- The returned `Self::Reader` is a reusable raw read executor for that path and
condition set.
-- No range is selected by `Service::read`.
+- No range is selected by `Access::read`.
`OpRead` should no longer contain `BytesRange`:
@@ -137,7 +136,7 @@ pub struct OpRead {
`OpReader` remains an internal execution policy used by OpenDAL's public
`Reader`. It controls `concurrent`, `chunk`, `gap`, `prefetch`, and
-`content_length_hint`. It must not become part of the backend `Service::read`
+`content_length_hint`. It must not become part of the backend `Access::read`
contract.
`ReadOptions` and `ReaderOptions` should be converted differently:
@@ -278,15 +277,10 @@ does not require every service to optimize handle reuse
immediately.
range, and execution policy:
```rust,ignore
-async fn read_inner(
- ctx: OperationContext,
- srv: Servicer,
- path: String,
- opts: ReadOptions,
-) -> Result<Buffer> {
+async fn read_inner(acc: Accessor, path: String, opts: ReadOptions) ->
Result<Buffer> {
let (range, op_read, op_reader) = opts.into();
- let raw_reader = srv.read(&ctx, &path, op_read)?;
- let reader = Reader::new(ctx, srv, path, raw_reader, op_reader);
+ let (rp, raw_reader) = acc.read(&path, op_read).await?;
+ let reader = Reader::new(acc, path, raw_reader, op_reader, rp);
reader.read(range.to_range()).await
}
```
@@ -294,19 +288,14 @@ async fn read_inner(
`Operator::reader_options` should create the reusable raw reader once:
```rust,ignore
-async fn reader_inner(
- ctx: OperationContext,
- srv: Servicer,
- path: String,
- opts: ReaderOptions,
-) -> Result<Reader> {
+async fn reader_inner(acc: Accessor, path: String, opts: ReaderOptions) ->
Result<Reader> {
let (op_read, op_reader) = opts.into();
- let raw_reader = srv.read(&ctx, &path, op_read)?;
- Ok(Reader::new(ctx, srv, path, raw_reader, op_reader))
+ let (rp, raw_reader) = acc.read(&path, op_read).await?;
+ Ok(Reader::new(acc, path, raw_reader, op_reader, rp))
}
```
-For services where `Service::read` currently performs remote I/O, the returned
+For services where `Access::read` currently performs remote I/O, the returned
raw reader should be lazy. This preserves the lazy reader behavior: creating
`op.reader(path)` should not require opening the underlying data request.
@@ -316,7 +305,7 @@ raw reader should be lazy. This preserves the lazy reader
behavior: creating
```rust,ignore
pub struct ReadContext {
- srv: Servicer,
+ acc: Accessor,
path: String,
args: OpRead,
options: OpReader,
@@ -325,19 +314,20 @@ pub struct ReadContext {
}
```
-`srv`, `path`, and `args` are still useful for planning, especially when
+`acc`, `path`, and `args` are still useful for planning, especially when
OpenDAL needs `stat` to resolve unbounded ranges for chunked reads or
`AsyncSeek` adapters. Actual raw range I/O should go through `reader.open` or
-`reader.read`, not through repeated `srv.read` calls.
+`reader.read`, not through repeated `acc.read` calls.
`Reader::metadata()` remains cache-only. The cache is updated from `RpRead`
-returned by `open` or `read`. Lazy backends may return `RpRead::default()` from
-the raw reader until the first raw range operation. Public `Reader::fetch`
-observes metadata from the planned raw `read` calls it executes.
+returned by `Access::read`, `open`, or `read`. Lazy backends may return
+`RpRead::default()` from `Access::read` and fill metadata after the first raw
+range operation. Public `Reader::fetch` observes metadata from the planned raw
+`read` calls it executes.
## Layers
-Existing layers continue to wrap `Service::read`, but the wrapping point moves
+Existing layers continue to wrap `Access::read`, but the wrapping point moves
from a range stream to a reusable raw reader.
### Type eraser
@@ -345,10 +335,11 @@ from a range stream to a reusable raw reader.
The type eraser should erase raw readers using the chosen object-safe
container:
```rust,ignore
-fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) ->
Result<oio::Reader> {
+async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead,
oio::Reader)> {
self.inner
- .read(ctx, path, args)
- .map(|r| Box::new(r) as oio::Reader)
+ .read(path, args)
+ .await
+ .map(|(rp, r)| (rp, Box::new(r) as oio::Reader))
}
```
@@ -358,7 +349,7 @@ specific type alias.
### Complete layer
-`CompleteLayer` previously read `args.range().size()` in `Service::read` and
+`CompleteLayer` currently reads `args.range().size()` in `Access::read` and
wraps the returned range stream. After this RFC, range size is only known when
`open` or `read` is called.
@@ -380,13 +371,13 @@ every range operation:
### Retry layer
-`RetryLayer` previously stored `OpRead` and advanced `args.range_mut()` after
+`RetryLayer` currently stores `OpRead` and advances `args.range_mut()` after
each successful stream read. After this RFC, range progress must move from
`OpRead` into range operation wrappers.
The new retry model should be:
-- retry `Service::read(path, OpRead)` while creating the reusable raw reader;
+- retry `Access::read(path, OpRead)` while creating the reusable raw reader;
- retry `raw_reader.open(range)` when opening a range stream fails before bytes
are returned;
- wrap the returned `ReadStream` and track bytes already emitted;
@@ -400,7 +391,7 @@ for the same path and read condition set.
### Correctness check layer
`CorrectnessCheckLayer` can keep checking `read_with_*` capabilities in
-`Service::read`, because those options still belong to the path and read
+`Access::read`, because those options still belong to the path and read
condition set.
## Presign read
@@ -429,7 +420,7 @@ OpReader)` and discards `OpReader`.
Services like `s3`, `gcs`, `azblob`, `oss`, and `http` should return a raw
reader that stores `core`, `path`, and `OpRead`.
-Each `open(range)` sends the same request that the current `Service::read`
sends
+Each `open(range)` sends the same request that the current `Access::read` sends
today. For example, S3's current logic:
```rust,ignore
@@ -475,8 +466,8 @@ public `Reader` keep the same user-facing behavior.
The raw API is breaking:
-- third-party `Service` implementations must remove range handling from
- `Service::read`;
+- third-party `Access` implementations must remove range handling from
+ `Access::read`;
- current range-scoped readers must become `ReadStream`;
- services must return reusable raw readers;
- layers must wrap reusable raw readers and range operation results.
@@ -486,7 +477,7 @@ Migration can be staged mechanically:
1. Introduce `ReadStream` and the new raw `Read` trait shape.
2. Move `BytesRange` out of `OpRead`.
3. Update type erasure and core read context.
-4. Convert object-store services by moving current `Service::read` request
logic
+4. Convert object-store services by moving current `Access::read` request logic
into `open(range)`.
5. Convert memory-like services by slicing in `read(range)`.
6. Convert handle based services first with independent per-range opens, then
@@ -509,9 +500,9 @@ The implementation should be considered complete when:
# Drawbacks
-- This is a breaking raw API change. Third-party `Service` implementations must
+- This is a breaking raw API change. Third-party `Access` implementations must
be updated.
-- It touches many services because every `Service::read` implementation
currently
+- It touches many services because every `Access::read` implementation
currently
receives a range.
- The rename from range-scoped `oio::Read` to `oio::ReadStream` is large but
necessary to avoid overloading one trait with two meanings.
@@ -534,7 +525,7 @@ progress, and request sizing into core and layers. OpenDAL
already has
`open(range)` for streaming and backpressure. The raw `read(range)` primitive
should instead express a complete, core-planned, bounded range read.
-## Add `Service::open_read_at`
+## Add `Access::open_read_at`
Rejected. It adds a parallel backend operation for an execution optimization.
OpenDAL's raw operation surface should stay centered on semantic operations:
@@ -561,10 +552,10 @@ ranges from it gives one stream two responsibilities and
makes layer wrapping
harder. The reusable raw reader should be the place that knows how to execute
additional ranges.
-## Add `ReadPlan` beside `Service::read`
+## Add `ReadPlan` beside `Access::read`
Rejected. It improves execution planning but keeps the old range-scoped
-`Service::read` as the primary contract. This RFC fixes the raw contract
instead
+`Access::read` as the primary contract. This RFC fixes the raw contract instead
of adding a second planning hook.
# Prior art
@@ -577,7 +568,7 @@ reduce extra `open` syscalls, and listed future
`read_ranges` and native
`read_at` support.
The current raw contract did not fully realize that intent because range
-selection stayed in `Service::read`. This RFC keeps RFC-4382's public direction
+selection stayed in `Access::read`. This RFC keeps RFC-4382's public direction
and moves the raw contract to the place where handle reuse and batch range
reads
can be implemented.