-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: Implement an AsyncReader for avro using ObjectStore #8930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
4ed172b
feat: Implement an AsyncReader for avro using ObjectStore
EmilyMatt e5c7f57
Merge branch 'main' into avro-async-reader
EmilyMatt f5bfd35
feature gate use
EmilyMatt 32b0760
update comments
EmilyMatt 2251be5
file size is mandatory
EmilyMatt 79af114
finish immediately
EmilyMatt 4e207ea
remove object store form default
EmilyMatt e04337c
remove object store form default
EmilyMatt 854cd95
Merge branch 'main' into avro-async-reader
EmilyMatt 8e0e46e
Use builder pattern, fallback to get the schema from the arrow schema…
EmilyMatt 4f88571
Merge branch 'main' into avro-async-reader
EmilyMatt cb19fad
remove accidental changes
EmilyMatt d59a2f5
Merge branch 'apache:main' into avro-async-reader
EmilyMatt e57801d
Merge branch 'refs/heads/main' into avro-async-reader
EmilyMatt 939cf8e
rebase, address CR
EmilyMatt 03de289
Merge remote-tracking branch 'upstream/avro-async-reader' into avro-a…
EmilyMatt a071ab8
fix some docs
EmilyMatt 1cc8d8a
update cfg
EmilyMatt 1a2169b
Add some docs
EmilyMatt c6634ad
Add a basic roundtrip test
EmilyMatt f7cffc9
Merge branch 'main' into avro-async-reader
EmilyMatt 4361057
address CR, fix some bugs
EmilyMatt 94aae91
use smaller buffer
EmilyMatt 3216788
add error state
EmilyMatt 138e72d
address CR
EmilyMatt 977f619
add tests
EmilyMatt 3f51768
Move schema parsing up
EmilyMatt 69b7967
Merge branch 'main' into avro-async-reader
EmilyMatt e3d3624
Remove parquet diffs
EmilyMatt 61c44e4
fix various failures
EmilyMatt 4d15e02
seperate impl
EmilyMatt eef1b4d
Merge branch 'main' into avro-async-reader
EmilyMatt 5c93414
sync with main
EmilyMatt 7936756
add runnable example
EmilyMatt 5bd5241
rename to ReaderBuilder
EmilyMatt 3f1489e
Fix empty files
EmilyMatt 068128f
Revert debug log
EmilyMatt 863dc17
export only relevant objects
EmilyMatt 31ee0eb
Merge branch 'main' into avro-async-reader
EmilyMatt ab27d7b
Update object store version
EmilyMatt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| use crate::reader::async_reader::DataFetchFutureBoxed; | ||
| use std::ops::Range; | ||
|
|
||
| /// A broad generic trait definition allowing fetching bytes from any source asynchronously. | ||
| /// This trait has very few limitations, mostly in regard to ownership and lifetime, | ||
| /// but it must return a boxed Future containing [`bytes::Bytes`] or an error. | ||
|
EmilyMatt marked this conversation as resolved.
Outdated
|
||
| pub trait AsyncFileReader: Send + Unpin { | ||
| /// Fetch a range of bytes asynchronously using a custom reading method | ||
| fn fetch_range(&mut self, range: Range<u64>) -> DataFetchFutureBoxed; | ||
|
|
||
| /// Fetch a range that is beyond the originally provided file range, | ||
| /// such as reading the header before reading the file, | ||
| /// or fetching the remainder of the block in case the range ended before the block's end. | ||
| /// By default, this will simply point to the fetch_range function. | ||
| fn fetch_extra_range(&mut self, range: Range<u64>) -> DataFetchFutureBoxed { | ||
| self.fetch_range(range) | ||
| } | ||
| } | ||
|
EmilyMatt marked this conversation as resolved.
Outdated
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| use crate::codec::AvroFieldBuilder; | ||
| use crate::reader::async_reader::ReaderState; | ||
| use crate::reader::header::{Header, HeaderDecoder}; | ||
| use crate::reader::record::RecordDecoder; | ||
| use crate::reader::{AsyncAvroReader, AsyncFileReader, Decoder}; | ||
| use crate::schema::{AvroSchema, FingerprintAlgorithm}; | ||
| use arrow_schema::{ArrowError, SchemaRef}; | ||
| use indexmap::IndexMap; | ||
| use std::ops::Range; | ||
|
|
||
| /// Builder for an asynchronous Avro file reader. | ||
| pub struct AsyncAvroReaderBuilder<R: AsyncFileReader> { | ||
| pub(super) reader: R, | ||
| pub(super) file_size: u64, | ||
| pub(super) schema: SchemaRef, | ||
| pub(super) batch_size: usize, | ||
| pub(super) range: Option<Range<u64>>, | ||
| pub(super) reader_schema: Option<AvroSchema>, | ||
| } | ||
|
|
||
| impl<R: AsyncFileReader> AsyncAvroReaderBuilder<R> { | ||
|
EmilyMatt marked this conversation as resolved.
Outdated
|
||
| /// Specify a byte range to read from the Avro file. | ||
| /// If this is provided, the reader will read all the blocks within the specified range, | ||
| /// if the range ends mid-block, it will attempt to fetch the remaining bytes to complete the block, | ||
| /// but no further blocks will be read. | ||
| /// If this is omitted, the full file will be read. | ||
| pub fn with_range(self, range: Range<u64>) -> Self { | ||
| Self { | ||
| range: Some(range), | ||
| ..self | ||
| } | ||
| } | ||
|
|
||
| /// Specify a reader schema to use when reading the Avro file. | ||
| /// This can be useful to project specific columns or handle schema evolution. | ||
| /// If this is not provided, the schema will be derived from the Arrow schema provided. | ||
| pub fn with_reader_schema(self, reader_schema: AvroSchema) -> Self { | ||
| Self { | ||
| reader_schema: Some(reader_schema), | ||
| ..self | ||
| } | ||
| } | ||
|
|
||
| async fn read_header(&mut self) -> Result<(Header, u64), ArrowError> { | ||
| let mut decoder = HeaderDecoder::default(); | ||
| let mut position = 0; | ||
| loop { | ||
| let range_to_fetch = position..(position + 64 * 1024).min(self.file_size); | ||
|
EmilyMatt marked this conversation as resolved.
Outdated
|
||
| let current_data = self | ||
| .reader | ||
| .fetch_extra_range(range_to_fetch) | ||
| .await | ||
| .map_err(|err| { | ||
| ArrowError::AvroError(format!( | ||
| "Error fetching Avro header from object store: {err}" | ||
| )) | ||
| })?; | ||
| if current_data.is_empty() { | ||
| break; | ||
| } | ||
| let read = current_data.len(); | ||
| let decoded = decoder.decode(¤t_data)?; | ||
| if decoded != read { | ||
| position += decoded as u64; | ||
| break; | ||
| } | ||
| position += read as u64; | ||
| } | ||
|
|
||
| decoder | ||
| .flush() | ||
| .map(|header| (header, position)) | ||
| .ok_or_else(|| ArrowError::AvroError("Unexpected EOF while reading Avro header".into())) | ||
| } | ||
|
|
||
| /// Build the asynchronous Avro reader with the provided parameters. | ||
| /// This reads the header first to initialize the reader state. | ||
| pub async fn try_build(mut self) -> Result<AsyncAvroReader<R>, ArrowError> { | ||
| if self.file_size == 0 { | ||
| return Err(ArrowError::AvroError("File size cannot be 0".into())); | ||
| } | ||
|
|
||
| // Start by reading the header from the beginning of the avro file | ||
| let (header, header_len) = self.read_header().await?; | ||
| let writer_schema = header | ||
| .schema() | ||
| .map_err(|e| ArrowError::ExternalError(Box::new(e)))? | ||
| .ok_or_else(|| { | ||
| ArrowError::ParseError("No Avro schema present in file header".into()) | ||
| })?; | ||
|
|
||
| let root = { | ||
| let field_builder = AvroFieldBuilder::new(&writer_schema); | ||
| match self.reader_schema.as_ref() { | ||
| None => { | ||
| let devised_avro_schema = AvroSchema::try_from(self.schema.as_ref())?; | ||
| let devised_reader_schema = devised_avro_schema.schema()?; | ||
| field_builder | ||
| .with_reader_schema(&devised_reader_schema) | ||
| .build() | ||
| } | ||
|
EmilyMatt marked this conversation as resolved.
Outdated
|
||
| Some(provided_schema) => { | ||
| let reader_schema = provided_schema.schema()?; | ||
| field_builder.with_reader_schema(&reader_schema).build() | ||
| } | ||
| } | ||
| }?; | ||
|
|
||
| let record_decoder = RecordDecoder::try_new_with_options(root.data_type())?; | ||
|
|
||
| let decoder = Decoder::from_parts( | ||
| self.batch_size, | ||
| record_decoder, | ||
| None, | ||
| IndexMap::new(), | ||
| FingerprintAlgorithm::Rabin, | ||
| ); | ||
| let range = match self.range { | ||
| Some(r) => { | ||
| // If this PartitionedFile's range starts at 0, we need to skip the header bytes. | ||
| // But then we need to seek back 16 bytes to include the sync marker for the first block, | ||
| // as the logic in this reader searches the data for the first sync marker(after which a block starts), | ||
| // then reads blocks from the count, size etc. | ||
| let start = r.start.max(header_len.checked_sub(16).unwrap()); | ||
| let end = r.end.max(start).min(self.file_size); // Ensure end is not less than start, worst case range is empty | ||
| start..end | ||
| } | ||
| None => 0..self.file_size, | ||
|
EmilyMatt marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| let reader_state = if range.start == range.end { | ||
| ReaderState::Finished | ||
| } else { | ||
| ReaderState::Idle | ||
| }; | ||
| let codec = header.compression()?; | ||
| let sync_marker = header.sync(); | ||
|
|
||
| Ok(AsyncAvroReader::new( | ||
| self.reader, | ||
| range, | ||
| self.file_size, | ||
| decoder, | ||
| codec, | ||
| sync_marker, | ||
| reader_state, | ||
| )) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.