Skip to content

enhance: pin sealed read-snapshot view reads through frozen column - #53913

Merged
sre-ci-robot merged 2 commits into
milvus-io:masterfrom
congqixia:enhance/pin-sealed-read-snapshot-view-family
Oct 3, 2026
Merged

sre-ci-robot merged 2 commits into
milvus-io:masterfrom
congqixia:enhance/pin-sealed-read-snapshot-view-family

Conversation

@congqixia

Copy link
Copy Markdown
Contributor

Related to #53247

Perchunk chunk_data/chunk_view reads in the expression and chunk-reader hot loop still call segment accessors that re-capture the immutable PublishedSegmentState on every access. Phase 1 routed the metadata hot loop (chunk_size, num_rows_until_chunk, get_chunk_by_offset, num_chunk_data, get_row_count) through the request-scoped SegmentReadSnapshot, but the actual data and view reads kept paying one atomic_load plus two ref-count RMWs per chunk on sealed segments.

Route the view family through the already-pinned column obtained from GetDataScanResources so every data read derives from the same frozen generation as the chunk boundaries, with zero atomics and zero ref-count churn:

  • SegmentChunkReader::ChunkData / ChunkStringView
  • SegmentExpr::GetChunkData / GetChunkView / GetChunkViewsByOffsets / GetBatchViews / GetViewsByOffsets (including the Json conversion branch)

Migrate the sealed hot-loop call sites: SegmentChunkReader.cpp, Expr.h, CompareExpr.h, UnaryExpr.cpp, and the group-by path (SearchGroupByOperator + StrictGroupFilteredSearch). PhySearchGroupByNode captures the request snapshot once in its constructor and threads it into SealedDataGetter, mirroring how segment_ and search_info_ are bound.

Growing segments and non-pinned paths keep the existing per-call segment access through the same fallback helpers, so behavior is bit-for-bit identical; sealed segments now read the view family from the pinned snapshot with no per-chunk capture.

Verified with the segcore unittest binary: SegmentChunkReader, group-by, sealed read-snapshot, expression, and chunked-sealed suites all pass.

Perchunk chunk_data/chunk_view reads in the expression and chunk-reader
hot loop still call segment accessors that re-capture the immutable
PublishedSegmentState on every access. Phase 1 routed the metadata hot
loop (chunk_size, num_rows_until_chunk, get_chunk_by_offset,
num_chunk_data, get_row_count) through the request-scoped
SegmentReadSnapshot, but the actual data and view reads kept paying one
atomic_load plus two ref-count RMWs per chunk on sealed segments.

Route the view family through the already-pinned column obtained from
GetDataScanResources so every data read derives from the same frozen
generation as the chunk boundaries, with zero atomics and zero ref-count
churn:

- SegmentChunkReader::ChunkData<T> / ChunkStringView
- SegmentExpr::GetChunkData / GetChunkView / GetChunkViewsByOffsets /
  GetBatchViews / GetViewsByOffsets (including the Json conversion branch)

Migrate the sealed hot-loop call sites: SegmentChunkReader.cpp,
Expr.h, CompareExpr.h, UnaryExpr.cpp, and the group-by path
(SearchGroupByOperator + StrictGroupFilteredSearch). PhySearchGroupByNode
captures the request snapshot once in its constructor and threads it into
SealedDataGetter, mirroring how segment_ and search_info_ are bound.

Growing segments and non-pinned paths keep the existing per-call segment
access through the same fallback helpers, so behavior is bit-for-bit
identical; sealed segments now read the view family from the pinned
snapshot with no per-chunk capture.

Verified with the segcore unittest binary: SegmentChunkReader, group-by,
sealed read-snapshot, expression, and chunked-sealed suites all pass.

Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
@sre-ci-robot sre-ci-robot added the size/L Denotes a PR that changes 100-499 lines. label Sep 29, 2026
@mergify mergify Bot added dco-passed DCO check passed. kind/enhancement Issues or changes related to enhancement labels Sep 29, 2026
@sre-ci-robot

Copy link
Copy Markdown
Contributor

[ci-v2-notice]
Notice: New ci-v2 system is enabled for this PR.

To rerun ci-v2 checks, comment with:

  • /ci-rerun-code-check // for ci-v2/code-check
  • /ci-rerun-code-check-macos // for Code Checker MacOS (GitHub Actions)
  • /ci-rerun-build // for ci-v2/build
  • /ci-rerun-build-all // for ci-v2/build-all (multi-arch builds)
  • /ci-rerun-buildenv // for ci-v2/build-env (build milvus-env builder images; update .env after the new tag is ready)
  • /ci-rerun-ut-integration // for ci-v2/ut-integration, will rerun ci-v2/build
  • /ci-rerun-ut-go // for ci-v2/ut-go, will rerun ci-v2/build
  • /ci-rerun-ut-cpp // for ci-v2/ut-cpp
  • /ci-rerun-ut // for all ci-v2/ut-integration, ci-v2/ut-go, ci-v2/ut-cpp, will rerun ci-v2/build
  • /ci-rerun-e2e-default // for ci-v2/e2e-default
  • /ci-rerun-e2e-amd // for ci-v2/e2e-amd (e2e pool dispatcher)
  • /ci-rerun-e2e-dist-wp // for ci-v2/e2e-dist-wp (Tencent distributed woodpecker-service boundary)
  • /ci-rerun-build-ut-cov // for ci-v2/build-ut-cov (build + unit tests in one pipeline)
  • /ci-rerun-build-ut-cov-tcus // force TC-US build-ut-cov; respects Portal limit, never falls back to AWS
  • /ci-rerun-gosdk // for ci-v2/go-sdk (Go SDK E2E tests, ARM)
  • /ci-rerun-gosdk-std // for ci-v2/go-sdk-std (Go SDK E2E, standalone)
  • /ci-rerun-gosdk-dist-wp // for ci-v2/go-sdk-dist-wp (Go SDK E2E, distributed + Woodpecker service)

If you have any questions or requests, please contact @zhikunyao.

@sre-ci-robot

Copy link
Copy Markdown
Contributor

✅ CI Loop Results d7e2ec4

Stage Result Duration Tests
✅ Build SUCCESS 11.9min -
✅ Code-Check SUCCESS 5.4min -
✅ UT-Integration SUCCESS 38.3min -
✅ UT-GO SUCCESS 26.6min -
✅ UT-CPP-Cov SUCCESS 46.8min 9705 total, 9705 passed, 0 failed

Total: 92min | Pipeline | Artifacts

Overall Coverage: 77.0%
Diff Coverage: CPP 65.0% (104 hit, 56 miss, 160 measurable lines, 191 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 65.0% (104/160 measurable lines, 191 unmeasured)

@mergify mergify Bot added the ci-passed label Sep 29, 2026
PinWrapper<Span<T>>
GetChunkData(FieldId field_id, int64_t chunk_id) const {
if (snapshot_) {
auto column = snapshot_->GetDataScanResources(field_id).first;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Avoid rebuilding skip-metrics resources for every data read

The new data helpers consume only .first, but SegmentReadSnapshot::GetDataScanResources also constructs a FieldSkipMetricsView: its implementation copies the column shared_ptr twice and calls GetSkipMetricsList() before the unused .second is destroyed. Thus every chunk/view read still performs two reference-count increment/decrement cycles, and lazy storage-v2 columns also pay an acquire load and metrics lookup, so the hot paths do not get the zero-refcount access this change is intended to provide. Add a column-only snapshot accessor that returns a borrowed pointer/reference (the snapshot already owns the published state) and use it throughout this helper family, SegmentChunkReader, and group-by.

std::nullopt,
false);
false,
snapshot);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Carry the snapshot into strict phase-2 membership scans

The snapshot is passed only to the phase-1 data getter here. In the PerGroup branch, BuildGroupOffsets still creates a fresh SegmentChunkReader without calling SetSnapshot and reads chunk metadata directly from the segment, so its full raw-field scan continues to recapture published state for each chunk-size/data transition. Since this branch can scan total_data_cnt_ rows, it leaves the most expensive strict group-by path outside the optimization. Carry the snapshot in StrictGroupPhase2Context, pass it to BuildGroupOffsets, and bind both the reader and metadata lookups to that snapshot.

std::optional<DataType> json_type = std::nullopt,
bool strict_cast = false) {
bool strict_cast = false,
const segcore::SegmentReadSnapshot* snapshot = nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] Place the common snapshot argument before JSON-only options

Appending snapshot after the three defaulted JSON-specific parameters forces every scalar branch to pass the same std::nullopt, std::nullopt, and false placeholders, expanding simple one-line cases into most of the new switch plumbing. This helper is file-local, so move snapshot before those optional parameters or capture it in a local factory; the scalar cases can then remain direct calls without dummy arguments, and the JSON cases keep supplying only their real options.

@mergify mergify Bot removed the ci-passed label Sep 30, 2026
if (snapshot_) {
column = snapshot_->GetColumn(field_id_);
} else {
column = segment_.GetChunkedColumn(field_id_).get();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Keep the fallback column owner alive until pinning

When snapshot_ is null, GetChunkedColumn() returns a shared_ptr, but calling .get() here keeps only its raw pointer and destroys the temporary owner at the semicolon. A concurrent sealed-state publication can then retire that column before column->GetChunk() runs, causing a use-after-free in the supported non-pinned fallback. The previous implementation kept the shared_ptr alive through pinning. Retain an owning shared_ptr in the fallback branch until GetChunk() returns, and use a borrowed pointer only for the snapshot-backed branch.

// segment contract); snapshot-present paths read from the frozen column.
template <typename ViewType>
PinWrapper<std::pair<std::vector<ViewType>, ValidityView>>
GetBatchViews(FieldId field_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] Remove the duplicate sealed-view wrapper layer

GetBatchViews and GetViewsByOffsets only forward to the existing snapshot-aware GetChunkView and GetChunkViewsByOffsets helpers, or to segment aliases with the same behavior. All three call sites already execute only for sealed segments, so this additional API layer does not enforce a reachable contract and duplicates logic that must remain synchronized. Call the existing helpers directly at those sites, passing the range where needed, and remove both wrappers.

@sre-ci-robot

Copy link
Copy Markdown
Contributor

❌ CI Loop Results 0db229a

Stage Result Duration Tests
✅ Build SUCCESS 13.1min -
✅ Code-Check SUCCESS 5.4min -
❌ UT-Integration FAILURE 39.3min -
✅ UT-GO SUCCESS 24.0min -
✅ UT-CPP-Cov SUCCESS 55.0min 9705 total, 9705 passed, 0 failed

Total: 95min | Pipeline | Artifacts

Overall Coverage: 77.0%
Diff Coverage: CPP 67.3% (113 hit, 55 miss, 168 measurable lines, 202 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 67.3% (113/168 measurable lines, 202 unmeasured)

Failed Test Logs:

@congqixia
congqixia force-pushed the enhance/pin-sealed-read-snapshot-view-family branch from 0db229a to fe6cb0f Compare September 30, 2026 07:28
return true;
}
auto raw_chunk_count = segment.num_chunk_data(field_id);
auto raw_chunk_count = snapshot ? snapshot->num_chunk_data(field_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] Reuse the reader for snapshot-aware metadata

The new membership path selects between snapshot and segment separately for chunk count, row prefixes, and every chunk size, then immediately creates a SegmentChunkReader and binds that same snapshot. Move reader construction and binding before these checks and use NumChunkData, NumRowsUntilChunk, and ChunkSize; this removes duplicate generation-selection logic and prevents the metadata scan from drifting from the accessor's source.

const milvus::segcore::SegmentInternalInterface* segment_;
// Request-scoped sealed read snapshot, captured once in the constructor
// like segment_ / search_info_; null for growing / non-pinned paths.
const milvus::segcore::SegmentReadSnapshot* snapshot_{nullptr};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] Avoid caching a one-use snapshot alias

snapshot_ duplicates state already owned by query_context_ and is read only once in GetOutput, which can execute its output-producing path only once. Obtain a local shared snapshot from query_context_ at that use site and pass its pointer to SearchGroupBy; then remove this raw-pointer member and its constructor assignment so ownership and lifetime remain visible at the call.

Review feedback on the Phase-2 view-family pin found two hot paths that
still paid per-chunk shared_ptr refcount churn, and one that bypassed
the pinned snapshot entirely.

GetDataScanResources constructs a FieldSkipMetricsView alongside the
column, copying the column shared_ptr twice and touching the lazy
storage-v2 metrics list on every data read. Add a column-only snapshot
accessor that returns a borrowed pointer straight out of the frozen
published state (zero ref-count traffic), and route the per-chunk data,
view, validity, and group-by reads through it. The pair accessor stays
for the skip-metrics and take/scan paths that need the metrics view.

The strict group-by phase-2 membership scan created a SegmentChunkReader
without binding the request snapshot and read chunk boundaries from the
segment, so the PerGroup raw-field scan over total_data_cnt_ rows kept
recapturing published state per transition. Carry the snapshot in
StrictGroupPhase2Context and bind both the reader and the metadata
lookups in BuildGroupOffsets/ScanRawField.

Also move the snapshot argument before the JSON-only options in the
file-local CreateFieldGetter so scalar branches no longer pass dummy
placeholders.

Verified with the segcore unittest binary: group-by, chunk-reader,
sealed read-snapshot, expression, and chunked-sealed suites pass.

Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
@congqixia
congqixia force-pushed the enhance/pin-sealed-read-snapshot-view-family branch from fe6cb0f to 94f9f18 Compare September 30, 2026 08:26
// snapshot column; growing and non-pinned paths fall back to the segment.
template <typename ViewType>
PinWrapper<std::pair<std::vector<ViewType>, ValidityView>>
GetChunkView(FieldId field_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] Centralize the duplicated snapshot column adapters

The new GetChunkView and GetChunkViewsByOffsets blocks duplicate the type dispatch and JSON materialization already implemented by SegmentInternalInterface::chunk_view and chunk_views_by_offsets, while the same SpanBase conversion is repeated again in SegmentChunkReader and SealedDataGetter. This leaves multiple supported-type matrices that must stay synchronized and makes a new view type easy to add to one path but not the others. Extract the column-backed typed adapters once next to ChunkedColumnInterface, then let these callers only select the snapshot column or the existing segment fallback.

@sre-ci-robot

Copy link
Copy Markdown
Contributor

✅ CI Loop Results fe6cb0f

Stage Result Duration Tests
✅ Build SUCCESS 8.0min -
✅ Code-Check SUCCESS 6.7min -
✅ UT-Integration SUCCESS 39.8min -
✅ UT-GO SUCCESS 30.3min -
✅ UT-CPP-Cov SUCCESS 55.5min 9705 total, 9705 passed, 0 failed

Total: 98min | Pipeline | Artifacts

Overall Coverage: 77.0%
Diff Coverage: CPP 66.9% (111 hit, 55 miss, 166 measurable lines, 189 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 66.9% (111/166 measurable lines, 189 unmeasured)

@sre-ci-robot

Copy link
Copy Markdown
Contributor

✅ CI Loop Results 94f9f18

Stage Result Duration Tests
✅ Build SUCCESS 5.5min -
✅ Code-Check SUCCESS 4.2min -
✅ UT-Integration SUCCESS 38.8min -
✅ UT-GO SUCCESS 11.5min -
✅ UT-CPP-Cov SUCCESS 46.5min 9705 total, 9705 passed, 0 failed

Total: 71min | Pipeline | Artifacts

Overall Coverage: 77.0%
Diff Coverage: CPP 66.5% (109 hit, 55 miss, 164 measurable lines, 189 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 66.5% (109/164 measurable lines, 189 unmeasured)

@mergify mergify Bot added the ci-passed label Sep 30, 2026
@liliu-z

liliu-z commented Oct 3, 2026

Copy link
Copy Markdown
Member

/lgtm
/approve

@sre-ci-robot

Copy link
Copy Markdown
Contributor

[approval-status] effective-owner-approvals=1 [liliu-z(comment)]; do-not-merge/disable-approve-self=not-required; do-not-merge/doc-need-two-approve=disabled; ignored=[none]

@sre-ci-robot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: liliu-z

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

The pull request process is described 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

@sre-ci-robot
sre-ci-robot merged commit 2c1cef7 into milvus-io:master Oct 3, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved ci-passed dco-passed DCO check passed. kind/enhancement Issues or changes related to enhancement lgtm size/L Denotes a PR that changes 100-499 lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants