Skip to content

GH: [Go][Parquet]Handle errors to prevent panic - #3

Merged
czs007 merged 2 commits into
milvus-io:v17.0.0from
xiaocai2333:fix_panic
Apr 3, 2026
Merged

GH: [Go][Parquet]Handle errors to prevent panic#3
czs007 merged 2 commits into
milvus-io:v17.0.0from
xiaocai2333:fix_panic

Conversation

@xiaocai2333

@xiaocai2333 xiaocai2333 commented Dec 19, 2025

Copy link
Copy Markdown

Rationale for this change

Fixes: apache/arrow-go#613

What changes are included in this PR?

Return error correctly.

Are these changes tested?

Yes

Are there any user-facing changes?

No

Signed-off-by: Cai Zhang <cai.zhang@zilliz.com>
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose

Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename the pull request title in the following format?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

@github-actions

Copy link
Copy Markdown

❌ GitHub issue apache#613 could not be retrieved.

1 similar comment
@github-actions

Copy link
Copy Markdown

❌ GitHub issue apache#613 could not be retrieved.

@xiaocai2333 xiaocai2333 changed the title GH-613: [Go][Parquet]Handle errors to prevent panic GH: [Go][Parquet]Handle errors to prevent panic Dec 19, 2025
Fix three cases in doImportChildren where importChild errors were
silently discarded for STRUCT, DENSE_UNION, and SPARSE_UNION types,
causing nil pointer dereference (SIGSEGV) in downstream code when
a child import fails.

Also fix memory cleanup on import error:
- Add forceRelease to importAllocator to prevent double-free
- Release already-imported children when doImportChildren fails
- Call forceRelease in doImportArr on error to avoid leaks
- Remove per-import defer cleanup in doImport (now handled by doImportArr)

Regression test for milvus-io/milvus#48375, milvus-io/milvus#48383.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Cai Zhang <cai.zhang@zilliz.com>

@czs007 czs007 left a comment

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.

/lgtm

@czs007 czs007 left a comment

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.

/lgtm

@czs007
czs007 merged commit fcc11b4 into milvus-io:v17.0.0 Apr 3, 2026
11 of 24 checks passed
sre-ci-robot pushed a commit to milvus-io/milvus that referenced this pull request Aug 10, 2026
issue: #51981

## What

`storage.MergeSort` is the default mix-compaction path
(`dataNode.compaction.useMergeSort` defaults to `true`), reached via
`mix_compactor.go` -> `merge_sort.go:135`. Three changes:

1. **Heap holds k entries, not every in-flight row.** `enqueueAll`
pushed every qualifying row of the current record into the queue, so the
heap held the in-flight row count rather than the reader count. Each
input record is already sorted by the merge key, so the standard form
applies: one entry per reader, pop one and push one. At 30 readers x
4096 rows/record the heap goes from ~246k entries to 30.

2. **Merge keys resolved once per record.** The comparator did
`recs[x.ri].Column(fid).(*array.Int64).Value(x.i)` per side per
comparison -- a map lookup plus a type assert whose result is invariant
for a record. They are now extracted in `advanceRecord` (`Int64Values()`
for int64, the `*array.String` pointer for varchar; both O(1) and
copy-free).

3. **No per-row allocation.** `pq.Enqueue(&index{...})` escaped once per
row, and `container/heap`'s `Push(x any)` boxes regardless. Replaced by
a small typed value heap over the existing `rowIndex{ri, i int32}`
(added in #50817). `storage.PriorityQueue` had no other users repo-wide
and is removed, along with the `endPositions` bookkeeping.

The sibling `Sort` in the same file already received (2) and (3) in
#50817.

## Numbers

`BenchmarkMergeSort` (added here), 200k rows over `generateTestSchema()`
-- 20 fields including 5 vector types. Median of 3, `-benchtime 10x -cpu
1`:

| | before | after |
|---|---|---|
| time | 523.50 ms | **325.96 ms (1.61x)** |
| allocs | 302,617 | **102,599 (-66%)** |

The gain depends on how much `rb.Append` dominates.
`BenchmarkMergeSortVarcharKey`, 65k rows over a single varchar field:

| | before | after |
|---|---|---|
| time | 60.83 ms | **9.18 ms (6.63x)** |
| allocs | 65,642 | **91** |
| B/op | 6.11 MB | 4.84 MB (-21%) |

The varchar allocation count is the cleanest evidence: 65,642 is
approximately the 65,536 rows, i.e. one allocation per row; after is 91
in total, identical across all three runs.

## One deliberate behavior change

A k-way merge relies on each input record being sorted by the merge key.
That reliance is not new, but the old path did not fail uniformly on
disorder, so this is a narrowing and worth stating precisely.

`endPositions` already depended on sortedness for the general case: an
unsorted record could dequeue its smallest key first and trigger
`advanceRecord` while stale entries from the now-invalid record remained
queued (`RecordReader.Next` is borrow-scoped, `record_reader.go:20-21`),
which panics with `index out of range` -- the `[3,1,2]` shape.

But it did tolerate a narrower class. Because the old loop enqueued
every eligible row of a record, disorder whose maximum key still lands
on the last row (a single reader with key order `[2,1,3]`, so
`endPositions[ri]` is the final index) never triggered the early
`advanceRecord`: it was emitted correctly as 1,2,3 and `MergeSort`
returned success. Those inputs now return an error instead. The
narrowing is intended -- a bare k-way merge would emit them silently out
of order, and trading a loud failure for silent corruption is the one
outcome worth avoiding -- and `mix_compactor.go:473-484` gates
merge-sort to already-sorted segments, so the affected class is
vanishingly rare on real data.

To avoid trading a loud failure for silent corruption, the merge now
checks that the emitted key never decreases and returns a merr error
otherwise. The previous key is held in reusable buffers rather than
cloned per row -- cloning would reintroduce exactly the per-row
allocation this PR removes.

**On #48322 / #48449.** An earlier revision of this description named
the gate as their root cause and called for a reachability analysis.
Both parts have since been resolved, and neither the way that paragraph
assumed.

#48322 was an arrow-go use-after-free on STRUCT array deallocation,
fixed by milvus-io/arrow#3 and #48775 and verified on `master-20260429`.
#48449 carries the same title; its closure trail is inconclusive and
does not point at the gate either. Neither issue supports the
attribution.

The reachability analysis has been done, and the scenario it described
-- an `IsSorted=true` segment passing the gate "after `namespaceEnabled`
is turned on" -- is **not reachable**. `EnableNamespace` is written only
at collection creation (`ddl_callbacks_create_collection.go:217`); every
schema-altering path rebuilds the snapshot through
`coll.ToCollectionSchemaPB()`, which copies the stored value
(`collection.go:163`), so the assignment at `collection.go:216` is an
identity. And `sort_compaction.go:373-374` derives both sorted flags
from that same immutable setting (`isSorted := !isNamespaceSorted`), so
a segment's flag and the plan's merge key cannot disagree today.

The gate is nonetheless loose in *structure* -- it tests whether a
segment was sorted, not by which key -- so it is safe by coincidence
rather than by construction. This PR now tightens it.

## Gate tightened

`canMergeSort` (extracted from `Compact()` so it is unit-testable) now
requires the sorted flag that corresponds to *this plan's* merge key,
rather than either flag. A mismatch falls through to the existing
`mergeSplit` path before any segment rows are read; no new fallback
code.

Two reviewers asked for this. It is defense-in-depth, not a fix for an
observed production case -- as established above, the metadata-level
mismatch is not reachable today.

The failure that *is* left loud -- a segment whose flag matches but
whose bytes do not -- is now diagnosable. `MergeSort` reports
`ErrDataIntegrity` (`ErrStorage`'s own doc directs callers here; both
are non-retriable `SystemError`, so retry semantics and classification
are unchanged) with a `reader N record M row K` coordinate, and
`mergeSortMultipleSegments` logs the reader-index to segment-ID mapping
alongside it, so the reader index resolves to a segment.

**Not done:** degrading to `mergeSplit` when that error fires. Rationale
in the review thread -- briefly, the prior behavior for general disorder
was a panic, so a deterministic error is not a regression against it;
and a metadata/data inconsistency is a should-not-happen class where
silent degradation would hide the problem.

## Test coverage

`MergeSort` had no varchar-key test and no multi-field-key test, and
`TestMergeSort`'s ordering assertion only checked `Value(0)` of each
output record while the 64MB `batchSize` yielded a single batch. The
first commit adds those baselines and shows them green on the
pre-rewrite implementation; the second keeps them green.

- `TestMergeSort` -- every row checked, small batchSize to force
multiple batches
- `TestMergeSortVarcharKey`, `TestMergeSortByMoreThanOneField` --
previously uncovered paths
- `TestMergeSortPredicateCalledOncePerRow` -- pins predicate arity,
whose production implementation carries a side effect
(`segmentTotalRows`)
- `TestMergeSortUnsortedInputReturnsError` -- the ordering check
- `TestMergeSortUnsortedInputReportsLaterRecord`,
`TestMergeSortUnsortedInputReportsOffendingReader` -- pin the reported
coordinate: the first that the record number is computed rather than
always 0, the second that the reader index is too
- `TestCanMergeSort`, `TestCanMergeSortMatchesMergeKey` -- the gate,
both halves of the merge-key truth table plus the segment-count boundary
- `TestRowHeap`, `TestRowHeapSingleElement`

## Verification

- `./internal/storage/ -run
'TestMergeSort|TestRowHeap|TestSort|TestRadixSort'` -- pass
- `./internal/datanode/compactor/...` -- pass
- `pkg/util/merr` guard tests -- pass (an error code changed)
- `go build -tags dynamic` on both packages -- pass
- `make static-check` -- **not verified locally**; the local
golangci-lint binary is incompatible with the current Go toolchain
(`unsupported version: 2`). `go vet` on both packages reports only
pre-existing findings in files this PR does not touch. Relying on CI for
this one.

Two pre-existing failures in the full `./internal/storage/` package
(`TestAzureObjectStorage` timeout, `TestReadFile` nil deref) reproduce
on unmodified master and are unrelated to this change.

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

---------

Signed-off-by: bigsheeper <yihao.dai@zilliz.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
sunby pushed a commit to sunby/milvus that referenced this pull request Aug 20, 2026
…#51998)

issue: milvus-io#51981

## What

`storage.MergeSort` is the default mix-compaction path
(`dataNode.compaction.useMergeSort` defaults to `true`), reached via
`mix_compactor.go` -> `merge_sort.go:135`. Three changes:

1. **Heap holds k entries, not every in-flight row.** `enqueueAll`
pushed every qualifying row of the current record into the queue, so the
heap held the in-flight row count rather than the reader count. Each
input record is already sorted by the merge key, so the standard form
applies: one entry per reader, pop one and push one. At 30 readers x
4096 rows/record the heap goes from ~246k entries to 30.

2. **Merge keys resolved once per record.** The comparator did
`recs[x.ri].Column(fid).(*array.Int64).Value(x.i)` per side per
comparison -- a map lookup plus a type assert whose result is invariant
for a record. They are now extracted in `advanceRecord` (`Int64Values()`
for int64, the `*array.String` pointer for varchar; both O(1) and
copy-free).

3. **No per-row allocation.** `pq.Enqueue(&index{...})` escaped once per
row, and `container/heap`'s `Push(x any)` boxes regardless. Replaced by
a small typed value heap over the existing `rowIndex{ri, i int32}`
(added in milvus-io#50817). `storage.PriorityQueue` had no other users repo-wide
and is removed, along with the `endPositions` bookkeeping.

The sibling `Sort` in the same file already received (2) and (3) in
milvus-io#50817.

## Numbers

`BenchmarkMergeSort` (added here), 200k rows over `generateTestSchema()`
-- 20 fields including 5 vector types. Median of 3, `-benchtime 10x -cpu
1`:

| | before | after |
|---|---|---|
| time | 523.50 ms | **325.96 ms (1.61x)** |
| allocs | 302,617 | **102,599 (-66%)** |

The gain depends on how much `rb.Append` dominates.
`BenchmarkMergeSortVarcharKey`, 65k rows over a single varchar field:

| | before | after |
|---|---|---|
| time | 60.83 ms | **9.18 ms (6.63x)** |
| allocs | 65,642 | **91** |
| B/op | 6.11 MB | 4.84 MB (-21%) |

The varchar allocation count is the cleanest evidence: 65,642 is
approximately the 65,536 rows, i.e. one allocation per row; after is 91
in total, identical across all three runs.

## One deliberate behavior change

A k-way merge relies on each input record being sorted by the merge key.
That reliance is not new, but the old path did not fail uniformly on
disorder, so this is a narrowing and worth stating precisely.

`endPositions` already depended on sortedness for the general case: an
unsorted record could dequeue its smallest key first and trigger
`advanceRecord` while stale entries from the now-invalid record remained
queued (`RecordReader.Next` is borrow-scoped, `record_reader.go:20-21`),
which panics with `index out of range` -- the `[3,1,2]` shape.

But it did tolerate a narrower class. Because the old loop enqueued
every eligible row of a record, disorder whose maximum key still lands
on the last row (a single reader with key order `[2,1,3]`, so
`endPositions[ri]` is the final index) never triggered the early
`advanceRecord`: it was emitted correctly as 1,2,3 and `MergeSort`
returned success. Those inputs now return an error instead. The
narrowing is intended -- a bare k-way merge would emit them silently out
of order, and trading a loud failure for silent corruption is the one
outcome worth avoiding -- and `mix_compactor.go:473-484` gates
merge-sort to already-sorted segments, so the affected class is
vanishingly rare on real data.

To avoid trading a loud failure for silent corruption, the merge now
checks that the emitted key never decreases and returns a merr error
otherwise. The previous key is held in reusable buffers rather than
cloned per row -- cloning would reintroduce exactly the per-row
allocation this PR removes.

**On milvus-io#48322 / milvus-io#48449.** An earlier revision of this description named
the gate as their root cause and called for a reachability analysis.
Both parts have since been resolved, and neither the way that paragraph
assumed.

milvus-io#48322 was an arrow-go use-after-free on STRUCT array deallocation,
fixed by milvus-io/arrow#3 and milvus-io#48775 and verified on `master-20260429`.
milvus-io#48449 carries the same title; its closure trail is inconclusive and
does not point at the gate either. Neither issue supports the
attribution.

The reachability analysis has been done, and the scenario it described
-- an `IsSorted=true` segment passing the gate "after `namespaceEnabled`
is turned on" -- is **not reachable**. `EnableNamespace` is written only
at collection creation (`ddl_callbacks_create_collection.go:217`); every
schema-altering path rebuilds the snapshot through
`coll.ToCollectionSchemaPB()`, which copies the stored value
(`collection.go:163`), so the assignment at `collection.go:216` is an
identity. And `sort_compaction.go:373-374` derives both sorted flags
from that same immutable setting (`isSorted := !isNamespaceSorted`), so
a segment's flag and the plan's merge key cannot disagree today.

The gate is nonetheless loose in *structure* -- it tests whether a
segment was sorted, not by which key -- so it is safe by coincidence
rather than by construction. This PR now tightens it.

## Gate tightened

`canMergeSort` (extracted from `Compact()` so it is unit-testable) now
requires the sorted flag that corresponds to *this plan's* merge key,
rather than either flag. A mismatch falls through to the existing
`mergeSplit` path before any segment rows are read; no new fallback
code.

Two reviewers asked for this. It is defense-in-depth, not a fix for an
observed production case -- as established above, the metadata-level
mismatch is not reachable today.

The failure that *is* left loud -- a segment whose flag matches but
whose bytes do not -- is now diagnosable. `MergeSort` reports
`ErrDataIntegrity` (`ErrStorage`'s own doc directs callers here; both
are non-retriable `SystemError`, so retry semantics and classification
are unchanged) with a `reader N record M row K` coordinate, and
`mergeSortMultipleSegments` logs the reader-index to segment-ID mapping
alongside it, so the reader index resolves to a segment.

**Not done:** degrading to `mergeSplit` when that error fires. Rationale
in the review thread -- briefly, the prior behavior for general disorder
was a panic, so a deterministic error is not a regression against it;
and a metadata/data inconsistency is a should-not-happen class where
silent degradation would hide the problem.

## Test coverage

`MergeSort` had no varchar-key test and no multi-field-key test, and
`TestMergeSort`'s ordering assertion only checked `Value(0)` of each
output record while the 64MB `batchSize` yielded a single batch. The
first commit adds those baselines and shows them green on the
pre-rewrite implementation; the second keeps them green.

- `TestMergeSort` -- every row checked, small batchSize to force
multiple batches
- `TestMergeSortVarcharKey`, `TestMergeSortByMoreThanOneField` --
previously uncovered paths
- `TestMergeSortPredicateCalledOncePerRow` -- pins predicate arity,
whose production implementation carries a side effect
(`segmentTotalRows`)
- `TestMergeSortUnsortedInputReturnsError` -- the ordering check
- `TestMergeSortUnsortedInputReportsLaterRecord`,
`TestMergeSortUnsortedInputReportsOffendingReader` -- pin the reported
coordinate: the first that the record number is computed rather than
always 0, the second that the reader index is too
- `TestCanMergeSort`, `TestCanMergeSortMatchesMergeKey` -- the gate,
both halves of the merge-key truth table plus the segment-count boundary
- `TestRowHeap`, `TestRowHeapSingleElement`

## Verification

- `./internal/storage/ -run
'TestMergeSort|TestRowHeap|TestSort|TestRadixSort'` -- pass
- `./internal/datanode/compactor/...` -- pass
- `pkg/util/merr` guard tests -- pass (an error code changed)
- `go build -tags dynamic` on both packages -- pass
- `make static-check` -- **not verified locally**; the local
golangci-lint binary is incompatible with the current Go toolchain
(`unsupported version: 2`). `go vet` on both packages reports only
pre-existing findings in files this PR does not touch. Relying on CI for
this one.

Two pre-existing failures in the full `./internal/storage/` package
(`TestAzureObjectStorage` timeout, `TestReadFile` nil deref) reproduce
on unmodified master and are unrelated to this change.

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

---------

Signed-off-by: bigsheeper <yihao.dai@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.

2 participants