Skip to content

refactor: spawn to read avro ObjectStore stream - #1

Merged
ariel-miculas merged 4 commits into
ariel-miculas:async-stream-readerfrom
mzabaluev:async-stream-read-with-runtime-rebased-on-ariel
May 11, 2026
Merged

refactor: spawn to read avro ObjectStore stream#1
ariel-miculas merged 4 commits into
ariel-miculas:async-stream-readerfrom
mzabaluev:async-stream-read-with-runtime-rebased-on-ariel

Conversation

@mzabaluev

Copy link
Copy Markdown

Use the runtime handle provided to the AvroObjectReader to spawn tasks that perform I/O operations on the ObjectStore.

Use the runtime handle provided to the AvroObjectReader to spawn
tasks that perform I/O operations on the ObjectStore.
use std::sync::Arc;
use tokio::runtime::Handle;

const STREAM_BUFFER_SIZE: usize = 8;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this magic number chosen? It shapes the backpressure profile of the streams, so some context on how this number is picked will help reason about the change.
(Or, can it be calculated with some heuristics based on the number of cores and/or available memory?)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not have any better idea than to not make the backpressure buffer too large, since the chunk size can be configured in object store implementations. For local filesystem this buffer of 8 chunks can grow up to 64 MiB, and the cloud backends typically chunk at 8-64 KiB. I don't think the number of cores should figure because it's typically one producer task and one consumer that drives the decoder per file. The channel buffer size could be 1 and still provide optimization, since the main idea is to parallelize I/O and decoding.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great context, could you please add that as code comment? I'm a big fan of documenting magic numbers

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. Another reason for keeping it small is the potential cancellation, as discussed in the other comment.

handle.spawn(async move {
while let Some(item) = stream.next().await {
let send_res = sender.send(item).await;
if send_res.is_err() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this mean we lose data in this scenario? What are the implications of a channel that's closed prematurely?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encountering an error here means the receiver part has been dropped without consuming all the items, so it's just propagating the cancellation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that I know. But what's the implication of a closed consumer before the producer is finished here? Does it mean we lose data? Do files that should have been decoded are now thrown away silently?
If so, I believe this error should be louder, not a silent break. That's why I'm asking

@mzabaluev mzabaluev May 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a low-level reader library, the possibilities for reporting are limited by design. The crate does not use tracing or any other logger API.

I think it's OK to follow the general async Rust principle of silently canceling an async operation if the consumer task has dropped the future/stream. There is no way to use that data any more because the application is not interested in decoding any more record batches, and object GET requests are supposed to be idempotent, so nothing is persistently lost.

@EmilyMatt

Copy link
Copy Markdown

Wait dont we already have this mechanism via the object store itself?

@mzabaluev

Copy link
Copy Markdown
Author

Wait dont we already have this mechanism via the object store itself?

Not explicitly in the implementations provided in the object_store crate. This is the HTTP client used for AWS S3 among others: https://github.com/apache/arrow-rs-object-store/blob/main/src/client/get.rs
The returned stream is formed with stream::try_unfold and is driven by the API consumer; there may be an underlying background task spawned by the HTTP client, but it's not clear cut.

This is done to resolve comments in apache#9668 (comment)

@mzabaluev

Copy link
Copy Markdown
Author

Note that the rationale in the doc of with_runtime method stipulates that all I/O bound tasks performed by the object store backend should be separate from Avro decoding. The decoding is performed by the application's async task driving the reader, and so it's up to the application to shunt it onto a different runtime, or just let the same multi-threaded runtime utilize a different CPU core.

The issue with the original implementation in apache#9632 is that we didn't use the spawn mechanism to read the byte stream, mainly just due to expediency.

@mzabaluev

Copy link
Copy Markdown
Author

Wait dont we already have this mechanism via the object store itself?

Finally, this behavior is conditional on whether with_runtime was used when constructing the AvroObjectReader. So if we know the object store impl does background I/O, we are good by default. So these changes are just to move the apache PR forward.

@ariel-miculas

Copy link
Copy Markdown
Owner

LGTM, though I can't say whether this is what Andrew Lamb was objecting, I guess we'll find out

.boxed(),
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would add a comment here explaining that we need to spawn (via the runtime handle) both the initial retrieval of the handle to the stream and all subsequent item retrievals via the .next calls; and this is the reason why the mpsc::channel is needed; and this is the main difference between spawn and spawn_stream. It may be obvious, but it would be nice to explicitly state

@ariel-miculas
ariel-miculas merged commit 5d5b3c2 into ariel-miculas:async-stream-reader May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants