enhance: route arrow Status to finer LOON FFI error codes - #568
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: czs007 The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Every arrow failure crossing the loon FFI boundary collapsed to LOON_ARROW_ERROR,
so the milvus consumer could not tell a transient failure from a permanent one and
had to treat them all the same. Add ArrowStatusToLoonCode (ffi_internal/result.h)
and route the RETURN_ERROR sites through it:
- IsOutOfMemory -> LOON_MEMORY_ERROR (transient, retriable)
- IsIOError -> LOON_IO_ERROR (transient object-storage
failure: throttling/timeout/reset; retriable)
- IsInvalid/TypeError/KeyError -> LOON_DATA_ERROR (malformed/corrupt data on
disk; permanent)
- everything else -> LOON_ARROW_ERROR
Add LOON_IO_ERROR / LOON_DATA_ERROR to the FFI enum (ffi_c.h) and their strings.
Applied across all FFI entry points (reader/writer/segment/manifest/exttable/
filesystem). Pairs with the milvus-side consumer that maps these LOON codes onto
the matching segcore ErrorCode (FileReadFailed / DataFormatBroken / ...).
Add test/ffi/result_test.cpp covering ArrowStatusToLoonCode and the new strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (15.95%) is below the target coverage (60.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #568 +/- ##
=======================================
Coverage 74.93% 74.93%
=======================================
Files 160 160
Lines 15355 15359 +4
Branches 2340 2341 +1
=======================================
+ Hits 11507 11510 +3
- Misses 3848 3849 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Review: right direction, but the classification is done at the FFI boundary while the error category is already destroyed in the core/format layersThis PR splits One guiding principle (for all follow-up changes)
P0 — correctness bugs that exist today (and that this PR will misclassify or be bypassed by)1. auto result = MakeArrowFileReader(*fs, path, ...); // opens a parquet file on object storage
if (!result.ok()) {
return arrow::Status::Invalid(fmt::format("Error making file reader ... {}", result.status().ToString()));
}The most common failure of 2. auto status = init(...);
if (!status.ok()) { LOG_STORAGE_ERROR_ << ...; throw std::runtime_error(status.ToString()); }The 3.
Fix: change 4. 5. P16. 7. return arrow::Status::IOError("ColumnGroup::AddRecordBatch: batch is null");A null batch is a logic error, but after this PR it becomes 8. 9. Local catching of external-system exceptions must be completed and made uniform 10. Fault-injection 11. Two gaps at the FFI boundary
In one sentenceThis PR does the classification at the FFI boundary, but |
Follow-up: a full-chain retryability audit — the codebase misclassifies in both directions depending on the formatAfter going through every First, the correct baseline: ParquetThe Parquet read path is essentially right and is the model to copy. It uses The systemic problem: every other path either drops the code or hard-codes one
So Packed / FS-init freeze transient errors into permanent ones (won't retry when they should), while Vortex / Lance treat permanent errors as transient (retry forever on corrupt data). Both violate the retry-vs-don't-retry contract, in opposite directions. Root cause for Vortex/Lance: the Rust bridge throws away the category (can't be fixed in C++)class VortexException : public std::runtime_error { ... }; // message string only, no kind
class LanceException : public std::runtime_error { ... }; // sameThe Rust side is built on opendal ( This means the Vortex/Lance retry classification cannot be fixed at the C++ layer — the fix has to be in the Rust bridge: surface the opendal Smaller but real
Two items to add to the plan
One sentenceThe retry contract this PR is trying to establish at the FFI boundary is undermined upstream by a layer that classifies in both wrong directions — Packed/FS-init force transient → permanent, Vortex/Lance force permanent → transient — and the Vortex/Lance half is structurally unfixable in C++ because the Rust bridge discards opendal's |
| if (status.IsIOError()) { | ||
| return LOON_IO_ERROR; | ||
| } | ||
| if (status.IsInvalid() || status.IsTypeError() || status.IsKeyError()) { |
There was a problem hiding this comment.
result.h:84 maps arrow Invalid/TypeError/KeyError to LOON_DATA_ERROR, but these statuses originate from caller/config validation paths (schema import, ConvertWriterConfig, URI parse, policy create), not on-disk corruption. LOON_DATA_ERROR signals to callers that their stored data is permanently corrupt, so a user who merely passes a bad schema or misconfigured writer receives a data-corruption error instead of an actionable invalid-argument one. Route these statuses to a validation/invalid-argument code rather than LOON_DATA_ERROR.
| #define LOON_DATA_ERROR 14 | ||
| // Internal use only. Do not use LOON_ERRORCODE_MAX in caller code. | ||
| #define LOON_ERRORCODE_MAX 13 | ||
| #define LOON_ERRORCODE_MAX 15 |
There was a problem hiding this comment.
This PR adds LOON_IO_ERROR (13) and LOON_DATA_ERROR (14) and bumps LOON_ERRORCODE_MAX to 15 in ffi_c.h, but the Python mirror of this enum in python/milvus_storage/_ffi.py:27 still hardcodes LOON_ERRORCODE_MAX = 13 and omits both new codes. There is no runtime effect today because check_result only embeds the raw err_code in the message and never branches on the constants, but the mirror is now stale and will mislead anyone reading or extending the bindings. Add LOON_IO_ERROR/LOON_DATA_ERROR and set LOON_ERRORCODE_MAX = 15 in _ffi.py.
|
In ARROW, I think only two types of errors can be retried here:
I will correct this part of error in |
Follow-up: the deserialization layer signals data corruption via C++ exceptions, not
|
… milvus-storage#568 The loon FFI mapping (LoonResultToErrorCode + LOON_IO_ERROR / LOON_DATA_ERROR) hard-depends on milvus-io/milvus-storage#568, which is not yet merged. Worse, its upstream P0s (transient S3 IO rewritten to permanent, corrupt data labeled transient) mean the consumer-side mapping would only faithfully translate already-misclassified categories. Revert internal/core/src/storage/loon_ffi/util.cpp to its untyped throw so this PR no longer depends on milvus-io#568 and CI builds against the current pin; the loon typing returns as a follow-up once milvus-io#568 (with its P0 fixes) merges and the milvus-storage_VERSION pin is bumped. Also preserve the call-site context on the binlog read paths: DataCodec/Event now throw SegcoreError(err.get_error_code(), fmt::format("...: {}", err.what())) so the typed code (e.g. DataFormatBroken for a truncated binlog) AND the original message both survive, instead of dropping the context AssertInfo used to carry. issue: milvus-io#47420 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
…ability Errors returned to upper layers could say "retryable" but never "whose problem is this", and the retry bit was stored independently of the meaning, in three hand-synced tables. This adds the missing axis and makes the tables generated instead of hand-maintained. - One category per code: User / Transient / Permanent, crossing the C ABI as loon_ffi_error_category(). Retriability is now DERIVED (retryable == category == Transient), not a second stored bool. - Both code tables (LOON_* internal, ExtendStatusCode) move into X-macro lists in ffi_error_code.h. The exported loon_errcode_* constants, error_to_string, the category/retryable lookups, the ExtendStatusCode enum and its metadata are all generated from them, so a code cannot be classified two different ways in two different places. - The 11 internal LOON_* codes are classified for the first time: they were absent from the metadata table, so loon_ffi_is_retryable_errcode answered false for all of them by omission rather than by decision. - Codes are aligned with the AWS S3 / Aliyun OSS vocabulary, with the five deliberate divergences pinned by tests and documented. - New LOON_SOURCE_NOT_FOUND / LOON_SOURCE_ACCESS_DENIED: the same object-store condition is a system failure on an internally generated path and a user error on a path the user typed, and only the entry point knows which. Wired at loon_exttable_explore / loon_exttable_get_file_info, the two entry points that take a user-supplied location. - docs/error-codes.md enumerates every code with its category, retry verdict, S3/OSS equivalent and segcore mapping, plus an honest per-producer coverage table. Behaviour delta: loon_ffi_is_retryable_errcode(LOON_MEMORY_ERROR) now returns true (OOM is retriable, matching segcore 2034). Every other code keeps its previous verdict. No consumer reads these values yet, so the delta is inert today. Refs: milvus-io/milvus#50903 (the deferred "LOON_* enum-ization + category" item), milvus-io#574, milvus-io#575. Supersedes the classification half of milvus-io#568, whose IOError -> retriable mapping contradicts the conservative default milvus-io#574 settled on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HDgMok3nR8ZNugKWWQQK2 Signed-off-by: xiaofanluan <xiaofan.luan@zilliz.com>
… milvus-storage#568 The loon FFI mapping (LoonResultToErrorCode + LOON_IO_ERROR / LOON_DATA_ERROR) hard-depends on milvus-io/milvus-storage#568, which is not yet merged. Worse, its upstream P0s (transient S3 IO rewritten to permanent, corrupt data labeled transient) mean the consumer-side mapping would only faithfully translate already-misclassified categories. Revert internal/core/src/storage/loon_ffi/util.cpp to its untyped throw so this PR no longer depends on milvus-io#568 and CI builds against the current pin; the loon typing returns as a follow-up once milvus-io#568 (with its P0 fixes) merges and the milvus-storage_VERSION pin is bumped. Also preserve the call-site context on the binlog read paths: DataCodec/Event now throw SegcoreError(err.get_error_code(), fmt::format("...: {}", err.what())) so the typed code (e.g. DataFormatBroken for a truncated binlog) AND the original message both survive, instead of dropping the context AssertInfo used to carry. issue: milvus-io#47420 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
… milvus-storage#568 The loon FFI mapping (LoonResultToErrorCode + LOON_IO_ERROR / LOON_DATA_ERROR) hard-depends on milvus-io/milvus-storage#568, which is not yet merged. Worse, its upstream P0s (transient S3 IO rewritten to permanent, corrupt data labeled transient) mean the consumer-side mapping would only faithfully translate already-misclassified categories. Revert internal/core/src/storage/loon_ffi/util.cpp to its untyped throw so this PR no longer depends on milvus-io#568 and CI builds against the current pin; the loon typing returns as a follow-up once milvus-io#568 (with its P0 fixes) merges and the milvus-storage_VERSION pin is bumped. Also preserve the call-site context on the binlog read paths: DataCodec/Event now throw SegcoreError(err.get_error_code(), fmt::format("...: {}", err.what())) so the typed code (e.g. DataFormatBroken for a truncated binlog) AND the original message both survive, instead of dropping the context AssertInfo used to carry. issue: milvus-io#47420 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
… milvus-storage#568 The loon FFI mapping (LoonResultToErrorCode + LOON_IO_ERROR / LOON_DATA_ERROR) hard-depends on milvus-io/milvus-storage#568, which is not yet merged. Worse, its upstream P0s (transient S3 IO rewritten to permanent, corrupt data labeled transient) mean the consumer-side mapping would only faithfully translate already-misclassified categories. Revert internal/core/src/storage/loon_ffi/util.cpp to its untyped throw so this PR no longer depends on milvus-io#568 and CI builds against the current pin; the loon typing returns as a follow-up once milvus-io#568 (with its P0 fixes) merges and the milvus-storage_VERSION pin is bumped. Also preserve the call-site context on the binlog read paths: DataCodec/Event now throw SegcoreError(err.get_error_code(), fmt::format("...: {}", err.what())) so the typed code (e.g. DataFormatBroken for a truncated binlog) AND the original message both survive, instead of dropping the context AssertInfo used to carry. issue: milvus-io#47420 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
… milvus-storage#568 The loon FFI mapping (LoonResultToErrorCode + LOON_IO_ERROR / LOON_DATA_ERROR) hard-depends on milvus-io/milvus-storage#568, which is not yet merged. Worse, its upstream P0s (transient S3 IO rewritten to permanent, corrupt data labeled transient) mean the consumer-side mapping would only faithfully translate already-misclassified categories. Revert internal/core/src/storage/loon_ffi/util.cpp to its untyped throw so this PR no longer depends on milvus-io#568 and CI builds against the current pin; the loon typing returns as a follow-up once milvus-io#568 (with its P0 fixes) merges and the milvus-storage_VERSION pin is bumped. Also preserve the call-site context on the binlog read paths: DataCodec/Event now throw SegcoreError(err.get_error_code(), fmt::format("...: {}", err.what())) so the typed code (e.g. DataFormatBroken for a truncated binlog) AND the original message both survive, instead of dropping the context AssertInfo used to carry. issue: milvus-io#47420 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
What
Every arrow failure crossing the loon FFI boundary collapsed to
LOON_ARROW_ERROR, so a consumer (milvus segcore) could not tell a transient failure from a permanent one and had to treat them all as one opaque error.This PR adds an
ArrowStatusToLoonCodehelper (ffi_internal/result.h) and routes theRETURN_ERROR(LOON_ARROW_ERROR, ...)sites through it so the failure category survives the FFI boundary:IsOutOfMemory()LOON_MEMORY_ERRORIsIOError()LOON_IO_ERROR(new)IsInvalid()/IsTypeError()/IsKeyError()LOON_DATA_ERROR(new)LOON_ARROW_ERRORTwo new codes (
LOON_IO_ERROR,LOON_DATA_ERROR) are added to the FFI enum (ffi_c.h) with theirerror_to_stringentries. Applied across all FFI entry points (reader / writer / segment / manifest / exttable / filesystem).Why
The codes are consumed on the milvus side, which maps each LOON code onto the matching segcore
ErrorCode(LOON_IO_ERROR -> FileReadFailedretriable,LOON_DATA_ERROR -> DataFormatBrokenpermanent, ...). Without the finer LOON codes a transient S3 IO failure was indistinguishable from permanent data corruption and could not be retried / rerouted.Pairs with
The milvus-side consumer change (separate PR on milvus-io/milvus) that reads these LOON codes at segcore's loon FFI boundary. Context: milvus error-handling standardization, milvus-io/milvus#47420.
Verified
make build, produceslibmilvus-storage.so).clang-format-18clean.