Skip to content

enhance: add error-handling ratchet gate (abort/throw baseline burn-down) - #596

Merged
jiaqizho merged 5 commits into
milvus-io:mainfrom
czs007:enhance-error-handling-ratchet
Jul 29, 2026
Merged

enhance: add error-handling ratchet gate (abort/throw baseline burn-down)#596
jiaqizho merged 5 commits into
milvus-io:mainfrom
czs007:enhance-error-handling-ratchet

Conversation

@czs007

@czs007 czs007 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

Adds a CI ratchet for error-handling discipline: a checked-in per-file baseline of abort sites (ValueOrDie/ValueUnsafe/MoveValueUnsafe) and throw sites in production code (cpp/src + cpp/include, tests excluded), enforced by a lightweight two-layer check:

  1. Exact match against the committed baseline: any per-file count change fails CI — an increase must be fixed in code, a decrease must be recorded by regenerating the baseline (make -C cpp update-error-ratchet) in the same PR, so freed slack cannot silently grow back.
  2. Base-branch totals (pull requests): per-category totals must not increase versus the baseline recorded on the PR's base commit. This is the machine-enforced ratchet direction: regenerating a raised baseline inside the PR keeps layer 1 green but fails layer 2.

Baseline at introduction: abort = 143, throw = 59 (git-tracked files only, so vendored/generated trees are excluded automatically).

Why

Ring-1 discipline for this library is that failures are reported as arrow::Status with structured classification, not by aborting the process or leaking exceptions to consumers (milvus links this C++ directly). Individual sweeps keep fixing sites — #575 removed four unguarded ValueOrDie abort paths — but nothing prevents regressions, and the pattern keeps reappearing:

How to work with it

make -C cpp check-error-ratchet    # what CI runs (also: bash cpp/scripts/error_handling_ratchet.sh check)
make -C cpp update-error-ratchet   # regenerate the baseline after a burn-down

The failure output is a unified diff against the baseline plus instructions for both directions; the base-totals failure names the category and both totals.

Verification

  • check passes on the pristine tree; category totals reported (abort=143, throw=59).
  • Injecting a throw std::runtime_error fails layer 1 with a per-file diff (verified locally, then reverted).
  • The self-reference attack (add a throw → regenerate the baseline in the same PR → check against the pre-attack base) fails layer 2 with throw: 59 (base) -> 60 (this PR); a genuine burn-down against the same base passes (both verified locally).
  • The workflow is toolchain-free (checkout + bash); layer 2 anchors to github.event.pull_request.base.sha and distinguishes bootstrap (no baseline on base, verified via git cat-file) from real failures.

Not covered (honest scope) — this is a text-level ratchet, not a semantic linter:

  • the grep counts comments/strings too, so an equal-count swap (delete a commented throw, add a real one in the same file — or, for layer 2, in another file) is not caught;
  • grep -c counts matching lines, not call sites: two sites on one line count once, and a site added to an already-matching line does not trip the gate;
  • the checker runs from the PR checkout (standard in-repo lint-gate trust model — defense target is accidental regression; edits to the gate are visible in the diff);
  • migrating to clang-query later can reuse the same baseline flow.

issue: milvus-io/milvus#50903

🤖 Generated with Claude Code

…own)

Library code must report failures as arrow::Status/arrow::Result (tagged
with ExtendStatusDetail where classification matters), not by aborting the
process (ValueOrDie on an error path) or throwing exceptions across the
library boundary. Past sweeps fixed sites individually (milvus-io#575 removed four
unguarded ValueOrDie aborts) but nothing prevented regressions, and new
instances kept appearing in later work.

This adds a ratchet: cpp/scripts/error_handling_ratchet.sh counts abort
(ValueOrDie/ValueUnsafe/MoveValueUnsafe) and throw sites per git-tracked
production file (cpp/src + cpp/include, no test code) and diffs the
result against a checked-in baseline. CI fails on ANY divergence:

- count went up: the new code must return Status instead; the baseline
  is not to be raised.
- count went down: the burn-down is recorded by regenerating the baseline
  (update-error-ratchet target) in the same PR, so the slack cannot grow
  back later.

Baseline at introduction: abort=143, throw=59. Wired as a standalone
lightweight workflow (no toolchain deps) plus check-error-ratchet /
update-error-ratchet targets.

Verified locally: check passes on the pristine tree; injecting a
throw into cpp/src/properties.cpp fails the check with a per-file diff.

issue: milvus-io/milvus#50903

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: czs007
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 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.95%. Comparing base (4e697a0) to head (1651d0e).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #596      +/-   ##
==========================================
- Coverage   75.95%   75.95%   -0.01%     
==========================================
  Files         168      168              
  Lines       16672    16672              
  Branches     2510     2510              
==========================================
- Hits        12664    12663       -1     
- Misses       4008     4009       +1     
Flag Coverage Δ
cpp 78.65% <ø> (-0.01%) ⬇️
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.

@xaxys

xaxys commented Jul 27, 2026

Copy link
Copy Markdown

Adversarial review of this CI ratchet (script + workflow + baseline TSV) surfaced one high-severity gap in how the gate enforces its own promise, plus several medium/low issues around counting semantics and CI wiring — none block merge, but the PR description should be adjusted to match what the gate actually enforces.

High

  • cpp/scripts/error_handling_ratchet.sh:80 — The baseline check is self-referential: check diffs the current checkout's collect output against the baseline file in the same checkout, and the workflow (error-handling-ratchet.yml:21-24) never fetches or compares against the base branch's baseline. An author who adds a new throw can simply run update, committing a raised baseline alongside the code, and CI stays green — the "baseline is not to be raised / cannot silently grow back" guarantee is enforced only by reviewers noticing the .tsv change, not by the gate. Suggestion: have the workflow fetch the base branch's baseline and fail if any per-file/per-category count increases, or honestly downgrade the PR description to say the ratchet direction relies on review. (raised by xaxys, tinswzy)

Medium

  • .github/workflows/error-handling-ratchet.yml:9 — The paths filter (only cpp/** and the workflow itself) combined with a required status check would leave non-cpp PRs permanently pending. This is a known GitHub trap: PRs that don't match the paths never trigger the workflow, so a required check never reports and pure-Java/docs/Python PRs cannot merge; if the check is not required, the gate is bypassable. Suggestion: drop the paths filter (the script runs in seconds, so running it on every PR is cheap) or explicitly document that this check must not be marked required. (raised by tinswzy)

  • cpp/scripts/error_handling_ratchet.sh:55 — The bare-text grep counts throw/ValueOrDie occurrences in comments and strings, so an equal-count swap can let a real regression through. This was confirmed empirically: all 4 throw hits in azurefs.cc and the ValueOrDie hits in reader.h/writer.h are in comments, and they make up those files' baseline counts exactly. The PR's "Not covered (honest scope)" section already discloses comment counting; the undisclosed residual risk is masking — deleting a commented throw while adding a real one in the same file keeps the count unchanged and passes the exact-match diff (and conversely, unrelated comment edits can block CI). Suggestion: add the masking scenario to the "Not covered" section. (raised by tinswzy, xaxys)

  • cpp/scripts/error_handling_ratchet.sh:55grep -c counts matching lines, not call sites, which conflicts with the stated goal that any new abort site must fail CI. Two ValueOrDie( on one line count as 1, a new site added to an already-matching line does not trip the gate, and fixing one of two same-line sites during burn-down is not recorded as progress; the regex also misses ValueOrDie (…) with a space or an interposed comment. The script's header comment acknowledges per-line counting, but the PR statement "Any divergence from the baseline fails CI" does not disclose this escape hatch. Suggestion: document the same-line limitation alongside the other scope caveats. (raised by tinswzy, xaxys)

Low

  • cpp/scripts/error_handling_ratchet.sh:52 — Filenames with special characters would be silently counted as zero. With Git's default core.quotePath=true, non-ASCII paths come out of git ls-files quoted/escaped, the subsequent grep -c "$f" fails on the literal escaped name, and || true swallows the error. No such filenames exist in the repo today, so this is a zero-cost hardening: use git -c core.quotePath=false ls-files -z with while IFS= read -r -d '' f. (raised by tinswzy)

  • .github/workflows/error-handling-ratchet.yml:17 — The workflow declares no permissions, so GITHUB_TOKEN gets the repo default. The job only checks out code and runs a read-only script; add permissions: contents: read at the workflow or job level. (raised by tinswzy)

…e branch

Adopts the adversarial-review findings on milvus-io#596:

- High (self-referential gate): 'check' now takes an optional base-branch
  baseline; in CI the pull_request job fetches the base ref's baseline
  and fails if any per-category TOTAL increased versus it. Regenerating
  a raised baseline inside the same PR keeps the exact-match layer green
  but fails this layer, so the no-new-sites direction is enforced by the
  gate itself, not by reviewers noticing a .tsv diff. Totals rather than
  per-file so moving grandfathered code between files stays neutral.
- Medium (paths-filter + required-check trap): dropped the paths filter;
  the script runs in seconds and now reports on every PR, with a comment
  explaining why.
- Medium x2 (comment counting can mask an equal-count swap; grep -c
  counts lines not call sites): disclosed both in the script header as
  known counting limits of the text-level gate.
- Low: git -c core.quotePath=false ls-files -z + read -d '' for special
  filenames; permissions: contents: read on the workflow.

Verified locally: plain check green; check against the current baseline
as base green; the attack path (add a throw, regenerate the baseline,
check against the pre-attack base) fails with the totals message; a
burn-down (one throw removed, baseline regenerated) passes against the
old base. Tree restored; committed baseline unchanged (abort=143,
throw=59).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
@czs007

czs007 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the adversarial pass — the High finding is real and is now fixed in 0832462; all six findings addressed:

  • High (self-referential gate): check now takes an optional base-branch baseline. The pull_request job fetches the base ref's error_handling_baseline.tsv and fails if any per-category total increased versus it — regenerating a raised baseline inside the PR keeps the exact-match layer green but fails this layer, so the ratchet direction is machine-enforced. Totals (not per-file) so moving grandfathered code between files stays neutral. Verified locally with the exact attack you described: add a throw → update → check against the pre-attack base → red (throw: 59 (base) -> 60 (this PR)); a genuine burn-down against the same base → green.
  • Medium (paths filter / required-check trap): paths filter dropped, with an in-file comment explaining why; the job costs seconds.
  • Medium (comment counting → equal-count masking) and Medium (line-counting, not call sites): both disclosed in the script header as known limits of a text-level ratchet (including the masking scenario and the same-line escape). Migrating to clang-query later can reuse the same baseline flow.
  • Low (quotePath): git -c core.quotePath=false ls-files -z + read -d ''.
  • Low (permissions): permissions: contents: read added.

Baseline content unchanged (abort=143, throw=59).

@tinswzy

tinswzy commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Adversarial review verified 7 non-blocking issues (3 medium, 4 low) in the error-handling ratchet CI gate — all relate to CI reliability, known scope trade-offs, and edge-case correctness rather than defects in the core mechanism, which was independently confirmed to work.

Medium

  • .github/workflows/error-handling-ratchet.yml:30 — The git fetch --depth=1 origin "${{ github.base_ref }}" step has no fallback (the || true on line 31 only covers git show), so a transient fetch failure turns the whole (prospectively required) job red and blocks PRs unrelated to the change. Suggestion: decide the trade-off explicitly — adding a fallback silently degrades layer 2, while omitting it risks false-red from infra flakes. (raised by tinswzy)
  • .github/workflows/error-handling-ratchet.yml:31 — Layer 2 trusts the baseline TSV committed on the base branch (git show FETCH_HEAD:...) instead of recomputing from the base tree; if that TSV drifts from the actual base code (merge skew, or a red base push left unattended), the comparison baseline is wrong. Mitigated by the push trigger keeping the base-side layer 1 check in sync under normal operation. Suggestion: document this assumption, or recompute the base side when drift is a concern. (raised by tinswzy)
  • cpp/scripts/error_handling_ratchet.sh:98 — Layer 2 compares per-category totals only, so deleting a throw in file A while adding one in file B cancels out; with a regenerated baseline in the same PR, both layers pass and the new site is not caught. The script's comments declare this as an intentional trade-off, but the PR description claims "Any divergence from the baseline fails CI." Suggestion: soften the description to "per-category totals must not increase" rather than closing the window (a per-site diff with a move allowlist is beyond the ratchet's scope). (raised by chyezh, tinswzy)

Low

  • .github/workflows/error-handling-ratchet.yml:30 — Timing skew between the checkout's merge ref (snapshotted PR-merge tree) and the fetched base-branch tip can make layer 2 falsely fail (or miss regressions) when the base has just merged a burn-down. Suggestion: fetch ${{ github.event.pull_request.base.sha }} instead of the branch name so both sides anchor to the same base commit. (raised by chyezh, tinswzy)
  • .github/workflows/error-handling-ratchet.yml:34 — The checker script runs from the PR checkout, so a malicious PR could neuter the gate. This is the inherent trust model of all in-repo lint gates (the defense target is accidental regression, and such edits are visible in the PR diff), hence low/informational. Suggestion: state this trust boundary in a workflow comment; a stronger option is checking out the checker from the protected base SHA and scanning the PR tree. (raised by chyezh)
  • cpp/scripts/error_handling_ratchet.sh:78grep -c counts matching lines, not call sites: appending a new ValueOrDie( to an already-matching line, or placing two calls on one line, passes both layers. The script's comments acknowledge this, but the PR description's "Not covered (honest scope)" section omits it while claiming "Any divergence from the baseline fails CI." Suggestion: add this limitation to the description and soften the wording accordingly. (raised by tinswzy)
  • .github/workflows/error-handling-ratchet.yml:31 — The 2>/dev/null || true on git show is meant for bootstrap (base predates the ratchet) but also swallows real failures such as a renamed baseline path left hard-coded in the workflow; the resulting empty file makes the script silently skip layer 2 permanently, disabling the self-reference protection without any warning. Suggestion: distinguish bootstrap from other failures — only || true when the file is confirmed absent from the base tree, and emit a warning annotation otherwise. (surfaced during verification)

- Anchor layer 2 to the PR's recorded base commit
  (github.event.pull_request.base.sha) instead of the branch name, so
  both sides of the comparison agree even when the base tip just moved
  (timing-skew false reds/misses).
- Distinguish bootstrap from real failures: the baseline's absence from
  the base commit is verified with git cat-file before skipping layer 2
  (a renamed path or transient git failure no longer silently disables
  the self-reference protection); a fetch failure degrades to layer 1
  with a visible ::warning instead of blocking unrelated PRs.
- Document the two standing assumptions in the workflow: layer 2 trusts
  the committed base baseline (kept in sync by the push trigger), and
  the checker runs from the PR checkout (in-repo lint-gate trust model:
  defense target is accidental regression, not a malicious PR).

Script and baseline unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
@czs007

czs007 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Round-2 findings addressed — workflow hardening in 838ab6a, wording fixes in the PR description (just edited):

  • base.sha anchoring (timing skew): layer 2 now fetches github.event.pull_request.base.sha instead of the branch name — both sides of the comparison anchor to the same commit.
  • bootstrap vs real failure: the baseline's absence is now verified with git cat-file -e against the base commit before skipping layer 2. A renamed baseline path or a transient git show failure can no longer silently disable the self-reference protection; a fetch failure degrades to layer 1 with a ::warning annotation (explicit trade-off: blocking unrelated PRs on an infra flake costs more than briefly losing the base-totals layer — the exact-match layer still runs).
  • committed-baseline trust + in-repo gate trust model: both assumptions now documented in the workflow (base-side push trigger keeps the TSV honest; the checker running from the PR checkout defends against accidental regression, not a malicious PR — such edits are visible in the diff).
  • wording (equal-count swap across files, line-counting): the PR description no longer claims "any divergence fails CI"; it now states the two-layer semantics precisely (exact match against the committed baseline + per-category totals must not increase vs base) and lists both counting limitations in the honest-scope section.

Script and baseline unchanged; the core mechanism was independently confirmed working in this round.

Comment thread cpp/scripts/error_handling_baseline.tsv Outdated
throw cpp/include/milvus-storage/common/fiu_local.h 2
throw cpp/src/common/metadata.cpp 4
throw cpp/src/ffi/ffi_fiu_c.cpp 1
throw cpp/src/ffi/reader_c.cpp 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why are ValueOrDie / ValueUnsafe / MoveValueUnsafe considered technical debt? In extern "C" functions, we need logic like this to convert Arrow errors into FFI errors:

  auto fs_result = FilesystemCache::getInstance().get();
  RETURN_ARROW_ERROR_IF(fs_result.status(), LOON_ARROW_ERROR,
                        "Failed to obtain filesystem: ",
                        fs_result.status().ToString());
  auto fs = fs_result.ValueOrDie();

I think this is reasonable because the Result has already been checked before accessing its value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right that a guarded ValueOrDie after RETURN_ARROW_ERROR_IF is not a bug — the FFI files follow that pattern consistently (the July audit reached the same conclusion: all FFI-layer sites are guarded and safe). The baseline entry doesn't claim otherwise; being in the baseline means grandfathered, never blocks anything as-is.

Why they're still counted, three practical reasons:

  1. A text-level gate can't tell guarded from unguarded, and the unguarded flavor is exactly the P0 abort class: enhance: classify packed extend status codes into segcore error codes #575 removed four, and enhance: add PackedRecordBatchReader::Make factory; fix abort and code-destroying paths in packed #598 just found a fifth (file_reader.cpp, data-dependent process abort) that had been sitting next to the guarded idiom for a year. Counting all of them means any new ValueOrDie gets one reviewer glance at one diff line — that's the entire cost, since existing sites never trip the gate.
  2. The burn-down direction isn't "guarded sites are wrong", it's "prefer constructs that are safe by construction": ARROW_ASSIGN_OR_RAISE or std::move(*result) after the same .ok() check express the identical logic with no abort path to hand-verify. Where the current macro pattern is the clearest option (these FFI functions), staying at the baseline forever is a perfectly fine steady state.
  3. Keeping one category rather than a guarded/unguarded split keeps the gate a 30-line grep instead of a clang-query dependency.

That said, this is maintainer's call: if you'd prefer the FFI layer excluded from the abort category (e.g. drop cpp/src/ffi/ from the scope, where the guarded macro pattern is the house style), I'll make that change — the gate keeps its value for the library core either way.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As u said A text-level gate can't tell guarded from unguarded. So, I don't think the current PR can solve any problems. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair — for the abort category you've convinced me: since the gate can't distinguish your (legitimate, house-style) guarded idiom from an unguarded abort, all it does there is nag every FFI addition without ever identifying a dangerous one. Dropped in d3e77b2; the baseline is now throw-only. If we ever want abort coverage back it should be a clang-query check that actually understands the guard, not a grep.

The throw category doesn't have this problem, which is why the PR still stands: Ring-1 forbids the library from leaking exceptions at all, so in library code any throw is a violation — no guarded/unguarded distinction exists to blur, and textual counting is the semantic judgment. Concretely: #597 just burned down 23 throw sites (lance/iceberg bridges); this gate is the only thing that keeps them from creeping back one convenience-throw at a time, which is exactly how the previous 59 accumulated.

So the narrowed claim for this PR: it doesn't catch unguarded aborts (yours to review, as today), it does mechanically prevent exception-leak regressions. If that narrowed scope is acceptable, it's ready for another look.

The abort category (ValueOrDie/ValueUnsafe) is dropped: a text-level
gate cannot tell the guarded FFI idiom (.ok() check + macro return +
ValueOrDie, the house style in cpp/src/ffi/) from an unguarded abort
path, so counting them nagged every legitimate addition without
distinguishing dangerous ones. Unguarded aborts remain a review concern;
a clang-query-based check could reintroduce the category with real
semantic discrimination.

The throw category stays: Ring-1 forbids the library from leaking
exceptions, so in library code ANY throw is a violation -- textual
counting IS the semantic judgment there, and it is what prevents the
23 sites burned down in milvus-io#597 from creeping back.

Baseline regenerated: throw=59 only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
Comment thread cpp/scripts/error_handling_baseline.tsv Outdated
throw cpp/src/ffi/reader_c.cpp 1
throw cpp/src/ffi/v2_column_groups_builder.cpp 5
throw cpp/src/filesystem/azure/azurefs.cc 4
throw cpp/src/filesystem/gcp/gcp_filesystem_producer.cpp 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The throw in cpp/src/filesystem/gcp/gcp_filesystem_producer.cpp is not the real throw execption.

// Register cleanup on exit. atexit handlers must not throw, so log on failure.

Comment thread cpp/scripts/error_handling_baseline.tsv Outdated
throw cpp/src/ffi/ffi_fiu_c.cpp 1
throw cpp/src/ffi/reader_c.cpp 1
throw cpp/src/ffi/v2_column_groups_builder.cpp 5
throw cpp/src/filesystem/azure/azurefs.cc 4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as the gcp_filesystem_producer.cpp, not the real throw exception.

Can u import a tools which like clang-tidy which can detech the real throw case i guess...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both confirmed — comment-only mentions, and there were more (10 of the 59). Fixed in the commit just pushed: the collector now strips comments via gcc -fpreprocessed -dD -E -P (removes comments without expanding includes/macros, so the job stays toolchain-free — the runner's stock gcc suffices). Baseline drops 59 → 49, eliminating the whole comment false-positive class: azurefs.cc ×4, fiu_local.h ×2, the two *_filesystem_producer.cpp you flagged, reader_c.cpp, jni_utils.cpp — all gone; the remaining 49 are real throw expressions.

On clang-tidy/clang-query: the exact check is match cxxThrowExpr(), and I'd like to get there — but it needs a compile_commands.json, i.e. a fully configured conan toolchain in the CI job, which turns a 5-second gate into a heavy build job. The pragmatic split I'd propose: this comment-stripped textual gate as the fast always-on ratchet, and a clang-query pass wired into the existing check-tidy infrastructure (which already has the compile database locally) as a local/nightly deep check — happy to add that as a follow-up if you want it. The baseline flow is reusable either way.

Both flagged baseline entries (gcp_filesystem_producer.cpp,
s3_filesystem_producer.cpp) were the word 'throw' inside comments.
The collector now pipes each file through gcc -fpreprocessed -dD -E -P,
which removes comments without expanding includes or macros, keeping
the gate toolchain-free while eliminating the comment false-positive
class entirely: the baseline drops 59 -> 49, removing 10 comment-only
entries (azurefs.cc x4, fiu_local.h x2, gcp/s3 producers, reader_c,
jni_utils).

A clang-query cxxThrowExpr() check would be exact but requires a
compile_commands.json and thus a configured-toolchain CI job; noted in
the header as the upgrade path, reusing the same baseline flow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
@jiaqizho

Copy link
Copy Markdown
Collaborator

/lgtm

@jiaqizho
jiaqizho added this pull request to the merge queue Jul 29, 2026
Merged via the queue into milvus-io:main with commit d3eebb1 Jul 29, 2026
12 of 13 checks passed
xiaofan-luan added a commit to xiaofan-luan/milvus-storage that referenced this pull request Jul 30, 2026
…us-io#606)

## Problem

`main` currently fails its own error-handling ratchet, and every open PR
inherits the failure once CI merges it with main.

milvus-io#598 (`352d545`) removed the last `throw` from
`cpp/src/packed/column_group.cpp` — `ColumnGroup::Table()` now returns
`arrow::Result` — but did not regenerate
`cpp/scripts/error_handling_baseline.tsv`. The committed baseline still
claims one throw there, and the ratchet's first layer is an exact match:

```
-throw	cpp/src/packed/column_group.cpp	1
```

## Why it got through

milvus-io#598 branched from before milvus-io#596 added the gate, so there was no baseline
at its base commit and the workflow took its documented bootstrap path:

```
::notice::error-handling-ratchet: no baseline at base commit (bootstrap); base-totals layer skipped
```

The exact-match layer had nothing to compare against on that PR, so the
divergence only became visible after the merge. milvus-io#597 is in the same
position (branched pre-milvus-io#596) — it regenerated its baseline on rebase, so
it is fine, but the bootstrap hole is worth knowing about: a PR that
predates the gate can merge a stale baseline.

## Change

Deletes the one stale line. Content taken from the diff CI printed on
milvus-io#604, not from a local `update` run.

**Warning for whoever touches this next on macOS:** do not run
`cpp/scripts/error_handling_ratchet.sh update` there. The scan uses `gcc
-fpreprocessed -dD -E -P`, which Apple clang rejects with `unknown
argument`, so every file silently counts zero — `check` reports the
entire baseline as removed, and `update` would erase the file. Worth a
guard in the script (probe the preprocessor once and fail loudly, and
refuse to write an empty baseline); I can follow up with that if wanted.

Unblocks: milvus-io#597, milvus-io#603, milvus-io#604, and anything else merged with main after
`352d545`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_011HDgMok3nR8ZNugKWWQQK2

Signed-off-by: xiaofanluan <xiaofan.luan@zilliz.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants