Skip to content

enhance: make parquet open_async non-blocking asynchronous - #600

Open
jiaqizho wants to merge 1 commit into
milvus-io:mainfrom
jiaqizho:better-parquet-open-async
Open

enhance: make parquet open_async non-blocking asynchronous#600
jiaqizho wants to merge 1 commit into
milvus-io:mainfrom
jiaqizho:better-parquet-open-async

Conversation

@jiaqizho

Copy link
Copy Markdown
Collaborator

Previously, open_async only moved the synchronous Parquet open path onto a Folly executor. Opening a remote file could still block a worker while fetching the file size and footer, so the async API did not provide a fully non-blocking open flow.

The new path uses ParquetFileReader::OpenAsync and bridges the Arrow futures back to the Folly result. When a usable footer-size hint is available, it reads that suffix directly, parses the trailer and metadata, and fetches only the missing metadata range when the hint is too small. Encrypted files and unusable hints fallback to Parquet’s native asynchronous footer handling.

The non-blocking random-access interface now also supports asynchronous size lookup, backed by S3 CRT HeadObjectAsync with cached size reuse and existing error mapping. Open-time continuations are transferred onto the executor supplied through via(), while normal data reads return to their original I/O path after the reader has finished opening.

@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: jiaqizho
To complete the pull request process, please assign tedxu after the PR has been reviewed.
You can assign the PR to them by writing /assign @tedxu in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.20574% with 56 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.37%. Comparing base (dac5781) to head (4bc3068).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cpp/src/format/parquet/parquet_format_reader.cpp 73.30% 55 Missing ⚠️
...lvus-storage/filesystem/async_random_access_file.h 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #600      +/-   ##
==========================================
- Coverage   76.41%   76.37%   -0.05%     
==========================================
  Files         173      173              
  Lines       17561    17749     +188     
  Branches     2655     2681      +26     
==========================================
+ Hits        13419    13555     +136     
- Misses       4142     4194      +52     
Flag Coverage Δ
cpp 78.92% <73.20%> (-0.08%) ⬇️
python 44.45% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jiaqizho
jiaqizho force-pushed the better-parquet-open-async branch from bf9448e to 2d8b7a9 Compare July 29, 2026 03:31
return;
}

status = state->reader->finish_open(std::shared_ptr<::parquet::arrow::FileReader>(std::move(arrow_reader)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

finish_open() is called inside the OpenAsync completion callback here with no surrounding try/catch, but it can throw: finish_open -> create_row_group_infos -> try_build_row_group_infos -> RowGroupMetadata::Deserialize throws std::runtime_error / std::stoull on malformed row-group metadata (metadata.cpp:203,206). A thrown exception escapes the arrow Future callback instead of being converted to arrow::Status, so state->promise is never set and the caller observes a folly BrokenPromise (or, when driven inline, the raw C++ exception) rather than a cleanly failed future. Wrap the finalization body in try/catch and convert thrown exceptions to arrow::Status before calling complete().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ack

fmt::format("Parquet file size is negative. [path={}, file_size={}]", state->reader->path_, file_size)));
}
state->file_size = file_size;
state->reader->file_size_ = static_cast<uint64_t>(file_size);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The file size resolved by the async HEAD is written only to the transient reader's file_size_ here; MetaTrait::Payload has no size field and create_metadata_from_reader never stores it, so the resolved size never reaches the cached Metadata. When the caller-supplied kPropertyFileSize is 0 (the same case that required the async HEAD), cache-hit reconstruction via create_from_metadata opens with file_size=0, and Parquet's SerializedFile constructor calls source_->GetSize() unconditionally before the metadata check (parquet/file_reader.cc:304), forcing ObjectCrtInputFile::GetSize -> EnsureHeadObject to issue a blocking synchronous S3 HEAD even though the footer metadata is cached. Persist the resolved size (e.g. add it to Payload and prefer it in create_from_metadata) so reconstruction reuses it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ack

@jiaqizho
jiaqizho force-pushed the better-parquet-open-async branch from 2d8b7a9 to 1fa45cb Compare July 29, 2026 06:59
metadata = try_parse_footer_metadata(footer_buffer, reader_props);
}

ARROW_RETURN_NOT_OK(builder.Open(std::move(parquet_file), reader_props, metadata));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The cache-backed reconstruction path falls back to a synchronous builder.Open() that performs blocking footer I/O, and because the guard at line 363 is if (key_retriever) (retriever-existence, not footer encryption) any retriever-configured reader — plaintext included — takes this path, not just encrypted footers. This blocks the async executor thread on a network footer read, defeating the async-open contract, and is the limitation flagged by the existing FIXME. Reconstruct and open the footer asynchronously so retriever-configured readers don't stall the caller.

}

ctx->self->SetCachedContentLength(content_length);
ctx->future.MarkFinished(content_length);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When open_async's future is consumed with an inline executor (e.g. .via(&folly::InlineExecutor::instance())), FollyArrowExecutor forwards each TransferAlways continuation straight into executor_->add, so the next step runs inline inside the AWS callback that publishes the previous result. GetSizeAsync's HEAD callback calls ctx->future.MarkFinished (s3_filesystem.cpp:838) while AsyncHeadContext still holds the finalizer std::shared_lock, and the chained footer read then calls holder_->Lock() again (s3_filesystem.cpp:870), recursively acquiring a second shared_lock on the same finalizer mutex on one thread — undefined behavior, and a deadlock against a concurrent FinalizeS3() writer on a writer-preferring shared_mutex. The new footer-to-missing-metadata read pair self-chains the same way (both reads go through ReadAtAsyncInto, which also holds its lock across MarkFinished), so the result must be published after the client/finalizer guard is released in both GetSizeAsync and the read path, not only for the HEAD-to-GET hop.

@jiaqizho jiaqizho changed the title enhance: make Parquet open_async truly asynchronous enhance: make parquet open_async non-blocking asynchronous Aug 7, 2026
Previously, open_async only moved the synchronous Parquet open path onto a Folly executor. Opening a remote file could still block a worker while fetching the file size and footer, so the async API did not provide a fully non-blocking open flow.

The new path uses ParquetFileReader::OpenAsync and bridges the Arrow futures back to the Folly result. When a usable footer-size hint is available, it reads that suffix directly, parses the trailer and metadata, and fetches only the missing metadata range when the hint is too small. Encrypted files and unusable hints fallback to Parquet’s native asynchronous footer handling.

The non-blocking random-access interface now also supports asynchronous size lookup, backed by S3 CRT HeadObjectAsync with cached size reuse and existing error mapping. Open-time continuations are transferred onto the executor supplied through via(), while normal data reads return to their original I/O path after the reader has finished opening.

Signed-off-by: jiaqizho <jiaqi.zhou@zilliz.com>
@jiaqizho
jiaqizho force-pushed the better-parquet-open-async branch from 1fa45cb to 4bc3068 Compare August 12, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants