Repository navigation
[fix](be) Keep a requested constant value on a column reader cache hit - #68018
LuciferYang wants to merge 1 commit into
Conversation
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
|
run buildall |
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
|
/review |
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
There was a problem hiding this comment.
Automated review result: changes requested (static review of head 2e7d6a02e4c7b78403c1304856e7ab0aa4c0df71).
One blocking correctness issue is reported inline: the new physical-to-constant cache upgrade can feed ConstantColumnReader into eager physical inverted-index initialization and turn a previously successful hidden commit-TSO query into an initialization error.
Critical checkpoints:
- Goal, scope, and correctness: the focused cache suitability check fixes the reported physical-reader hit for ordinary row/segment-zone-map reads, and the same-key upsert fixes duplicate LRU nodes. It is not correct for the indexed continuation described inline.
- Concurrency and lifecycle: cache map/list mutation is mutex-protected;
splicepreserves the stored iterator; returnedshared_ptrs remain valid across replacement/eviction. Last-writer-wins construction creates no second distinct defect. - Configuration, compatibility, persistence, and writes: no new configuration, FE/BE field, RPC, serialized format, transaction/write/recovery behavior, or rolling-upgrade state is introduced.
- Parallel and special paths: constant data and segment-zone-map consumers are correct; page-zone-map, bloom, and later predicate-pruning paths are conservative. The eager inverted-index consumer is the unsafe exception. The separately tracked placeholder zone-map-builder gap in #67995 was not duplicated.
- Tests and results: the added unit tests distinguish the direct O-to-C cache fix and stale-LRU-node fix, but do not cover the physical-warm plus hidden-index continuation that exposes the inline issue. Per the review contract, no local build or tests were run. At this reviewed head, GitHub reports BE UT, COMPILE, P0, NonConcurrent, External Regression, Cloud regression, coverage, formatter, and style checks passed; performance remains pending. These are CI claims, not independent execution.
- Observability and performance: cache counters remain internally consistent with resident-key lookup semantics, and the upsert remains O(1); no additional substantive metric or hot-path regression was found.
- User focus: no additional review focus was supplied.
Review completion: complete after three bounded rounds. All candidates were independently verified and deduplicated; the accepted set is the single inline issue below, with no unresolved or capped finding.
| // reader would hand back the placeholder both as row data and as a zone map. Fall through and | ||
| // build the constant reader, replacing the cached entry so later callers get the real value too. | ||
| if (auto cached = _lookup({col_uid, {}}); | ||
| cached != nullptr && (!const_value.has_value() || cached->is_constant())) { |
There was a problem hiding this comment.
[P1] Keep constant readers out of physical index initialization. With an inverted index on __DORIS_COMMIT_TSO_COL__, an OR such as tso < 10 OR tso > 20 remains a common expression, so segment expression-zone-map setup first caches the physical reader. This condition then replaces it with ConstantColumnReader during column-iterator setup; the following bare, eager index lookup receives that constant and calls non-virtual ColumnReader::new_index_iterator even though its physical type/index state was never initialized, so the query fails. This is new: for a real TSO greater than 20, both placeholder 0 and the real value satisfy the OR, so the pre-PR physical-warmed path returned the correct projected rows. Please skip physical index creation for constant readers or separate the cache entries, and cover this ordering in a test.
There was a problem hiding this comment.
Removed the ConstantColumnReader::new_index_iterator override from this PR, and this thread is the reason. Making the reader refuse index init based on its dynamic type cannot be complete: cache eviction and a concurrent bare miss (the two sibling P1s here) both rebuild a physical reader for the same column, and the index path passes no constant, so the guard is bypassed. Residency and arrival order can't carry a correctness property.
The reliable signal is the schema: the column's ordinal is commit-TSO / version / binlog-TSO under known substitution conditions. #67995 makes Segment::new_index_iterator (and the zone-map builders and the pushed-agg gate) skip or synthesize based on that, which covers all three orderings at once and fixes the pre-existing single-slot exposure too. Moving the whole placeholder concern there rather than shipping a partial reader-type guard here.
This PR is now scoped to the cache contract only: a request carrying a constant is not dropped on a hit, and the entry is replaced in place. That part is complete and tested on its own.
TPC-H: Total hot run time: 16974 ms |
TPC-DS: Total hot run time: 82134 ms |
ClickBench: Total hot run time: 14.7 s |
|
Fixed, with one correction to the framing. The mechanism is real. It is not new to this PR, though. The predicate loop in I also could not confirm the premise that such an index can exist: The fix is on the reader rather than at the call site: The full ordering test you asked for needs a segment carrying an inverted index on that hidden column, which I could not build. What I have instead is the reader refusing regardless of how it reached index setup.
|
|
run buildall |
|
/review |
|
run beut |
|
run cloud_p0 |
|
run performance |
There was a problem hiding this comment.
Automated review result: changes requested (static review of exact head 3c7b4c426417d2f96beab15082a670fd7fc54d1b).
One blocking correctness issue is reported inline: the new same-key upsert allows an in-flight physical-reader miss to overwrite a completed constant-reader upgrade, so placeholder index or zone-map state can still drive filtering.
Critical checkpoints:
- Goal and proof: the sequential physical-to-constant cache upgrade and constant-reader index dispatch are fixed and unit-covered, but the stated authoritative-constant behavior is not preserved under concurrent misses.
- Scope: the four-file change is focused on reader polymorphism, cache replacement, and targeted BE unit tests.
- Concurrency: map/list operations are mutex-protected, but lookup, construction, and upsert form a semantic lost-update race; this is the blocking P1. No additional lock-order or deadlock issue was found.
- Lifecycle/static initialization: the non-null constant is the immutable commit TSO for one rowset/segment identity; reader/iterator ownership remains safe, with no cycle or cross-TU initialization issue.
- Configuration: no configuration item is added or changed.
- Compatibility/rolling upgrade: no wire, storage-format, RPC, or persisted-state change is introduced; the virtual dispatch change is internal to BE.
- Parallel paths: stable fresh-null inverted/ANN fallback retains row evaluation correctly. Bare index and expression-zone-map consumers must observe the authoritative constant, which the accepted race can violate. The existing sequential index-dispatch thread is not duplicated.
- Conditional checks: the new
is_constant()condition explains and handles the sequential cases, but it does not define the concurrent winner at insertion. - Test coverage: sequential replacement, one-node LRU accounting, constant zone-map/type behavior, and direct index no-op are covered. A barrier-controlled mixed physical/constant miss test and authoritative-winner return assertion are missing.
- Test results: no builds or tests were run locally because the review contract requires static-only review; no
.outfiles apply. - Observability: cache gauges remain balanced; no additional required log or metric was identified.
- Transaction/persistence: no EditLog, transaction, publish, or recovery data is changed.
- Data writes/crash safety: no write path or on-disk mutation is modified.
- Cross-layer variables: no new FE-BE variable or protocol field is introduced.
- Performance/memory: replacement remains O(1), avoids duplicate LRU nodes, and adds no row-loop work or ownership leak.
- Other issues: different-constant reuse and reused non-null index-output hypotheses were dismissed with call-path evidence; the separate bare placeholder zone-map issue is already tracked/fenced and was not duplicated.
User focus: no additional review focus was supplied.
Review completion: complete after two bounded rounds. The second normal and risk-focused reviews both returned NO_NEW_VALUABLE_FINDINGS; all candidates are accepted, dismissed with evidence, or duplicate-fenced. Accepted set: the single inline P1 below.
| // Replacing an existing key updates its node in place. Pushing a second node for the same key | ||
| // would leave the first one unreachable in the list while eviction erases the map entry of | ||
| // whichever copy reaches the tail, dropping the live reader from the map. | ||
| if (auto it = _cache_map.find(key); it != _cache_map.end()) { |
There was a problem hiding this comment.
[P1] Preserve the constant reader across concurrent misses. _lookup releases _cache_mutex before construction, so a bare request can miss and start building the physical reader, a constant request can then insert and return its reader, and the first request reaches this branch last and overwrites it. Since a cached Segment is shared, a query that already created a ConstantColumnIterator can then do the bare index lookup and receive the placeholder's physical index; for example, real commit TSO 42 with tso > 20 can be eliminated by an index containing 0, with the predicate removed from row fallback. This is the inverse concurrent order from the existing sequential comment. Please make the compare-and-upsert constant-dominant regardless of arrival order, return the selected authoritative reader to the caller, and add a barrier-controlled mixed physical/constant miss test.
There was a problem hiding this comment.
Removed the ConstantColumnReader::new_index_iterator override from this PR, and this thread is the reason. Making the reader refuse index init based on its dynamic type cannot be complete: cache eviction and a concurrent bare miss (the two sibling P1s here) both rebuild a physical reader for the same column, and the index path passes no constant, so the guard is bypassed. Residency and arrival order can't carry a correctness property.
The reliable signal is the schema: the column's ordinal is commit-TSO / version / binlog-TSO under known substitution conditions. #67995 makes Segment::new_index_iterator (and the zone-map builders and the pushed-agg gate) skip or synthesize based on that, which covers all three orderings at once and fixes the pre-existing single-slot exposure too. Moving the whole placeholder concern there rather than shipping a partial reader-type guard here.
This PR is now scoped to the cache contract only: a request carrying a constant is not dropped on a hit, and the entry is replaced in place. That part is complete and tested on its own.
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
TPC-H: Total hot run time: 16923 ms |
TPC-DS: Total hot run time: 83555 ms |
ClickBench: Total hot run time: 14.75 s |
|
/review |
There was a problem hiding this comment.
Automated review result: changes requested (static review of exact head 3c7b4c426417d2f96beab15082a670fd7fc54d1b).
One new blocking correctness issue is reported inline: ordinary LRU eviction can discard the only marker that commit TSO is logically constant between column-iterator and index-iterator setup, after which a bare lookup rebuilds and uses the placeholder's physical index.
The existing concurrent mixed-miss P1 remains valid in discussion_r4025665398 and is not duplicated. The earlier physical-index-dispatch P1 in discussion_r4017097815 is addressed by the new virtual constant no-op.
Critical checkpoints:
- Goal and proof: sequential physical-to-constant replacement, one-node LRU replacement, and resident-constant index fallback work, but constant authority is still lost after normal eviction and during the already-reported concurrent miss.
- Scope: the four-file change is focused on reader polymorphism, cache replacement, and targeted BE unit tests.
- Concurrency and lifecycle: map/list mutation is mutex-protected,
splicepreserves iterators, shared ownership is safe, and no deadlock, cycle, destructor, or static-initialization issue was found. The existing semantic lost-update race remains; the new inline issue is deterministic and single-threaded. - Configuration: no setting is added, but the existing mutable cache capacity makes the eviction failure deterministic at one and still possible at the default capacity on a sufficiently wide indexed scan.
- Compatibility and rolling upgrade: no wire, RPC, storage-format, or persisted-state change is introduced; the virtual dispatch is BE-internal.
- Parallel paths: scalar/common-expression pruning, statistics, row/rowid reads, page zone maps, bloom/dictionary fallback, repeated inverted/ANN setup, variant readers, and cache destruction were traced. Constant methods are safe while the constant reader remains selected; a bare post-eviction miss loses that selection. The separate pre-constant builder issue tracked by #67995 was not duplicated.
- Conditional checks:
is_constant()handles a resident sequential entry but cannot preserve semantics once that entry is absent. Preserving a non-null index output is intentional multi-index behavior, so an unconditional reset was rejected. - Tests and results: the added tests cover sequential replacement, LRU uniqueness/eviction, constant zone-map/type behavior, and an initially-null index no-op. They do not compose eviction between row and index setup; a low-capacity end-to-end regression is required. The mixed-miss barrier test remains requested by the existing P1. No builds or tests were run locally because this review contract is static-only; no result files apply.
- Observability: cache accounting remains balanced; no separate logging or metrics defect was found.
- Transactions, persistence, writes, and crash safety: no applicable path is changed.
- Cross-layer variables: no FE-BE field or protocol variable is added.
- Performance and memory: lookup/replacement remain O(1), virtual dispatch is outside row-hot loops, and no leak or unbounded retention was found; a fix should preserve bounded reader caching while retaining a small authoritative semantic marker if needed.
User focus: no additional focus was supplied.
Review completion: complete after three bounded rounds. The final cache/header, end-to-end/test, and risk-focused passes all returned NO_NEW_VALUABLE_FINDINGS; every candidate is accepted, dismissed with evidence, or duplicate-fenced. Accepted new set: the single inline P1 below.
| std::unique_ptr<IndexIterator>* /*iterator*/) override { | ||
| return Status::OK(); | ||
| } | ||
|
|
There was a problem hiding this comment.
[P1] Preserve constant semantics across cache eviction. The constant reader is an ordinary LRU entry, so with capacity 1 and read order [indexed c, indexed commit TSO], column setup leaves the constant cached, then c's bare index lookup evicts it and the following TSO lookup rebuilds the physical reader. This override is never reached; placeholder index 0 can eliminate real TSO 42 for tso > 20 while the retained row iterator yields 42. This is distinct from the existing races because it is single-threaded and the correct constant request already succeeded. Please derive the constant/no-index decision from StorageReadOptions (or retain non-evictable semantic state) and add a low-capacity end-to-end test.
There was a problem hiding this comment.
Removed the ConstantColumnReader::new_index_iterator override from this PR, and this thread is the reason. Making the reader refuse index init based on its dynamic type cannot be complete: cache eviction and a concurrent bare miss (the two sibling P1s here) both rebuild a physical reader for the same column, and the index path passes no constant, so the guard is bypassed. Residency and arrival order can't carry a correctness property.
The reliable signal is the schema: the column's ordinal is commit-TSO / version / binlog-TSO under known substitution conditions. #67995 makes Segment::new_index_iterator (and the zone-map builders and the pushed-agg gate) skip or synthesize based on that, which covers all three orderings at once and fixes the pre-existing single-slot exposure too. Moving the whole placeholder concern there rather than shipping a partial reader-type guard here.
This PR is now scoped to the cache contract only: a request carrying a constant is not dropped on a hit, and the entry is replaced in place. That part is complete and tested on its own.
3c7b4c4 to
2e7d6a0
Compare
|
/review |
There was a problem hiding this comment.
Automated review result: changes requested (static review of exact head 2e7d6a02e4c7b78403c1304856e7ab0aa4c0df71).
No new distinct inline comment is added in this pass: all substantiated failures are already covered by the existing review context. The current-head physical-to-constant/index failure remains in discussion_r4017097815, and the mixed-miss overwrite remains in discussion_r4025665398. The low-capacity eviction form is covered by discussion_r4032777651. The author's replies defer schema-derived protection to #67995, but that issue is still open and the protection is not present in this diff, so this head is not independently safe to merge.
Critical checkpoints:
- Goal and proof: the change correctly upgrades a sequential physical cache hit to a constant reader and repairs same-key LRU-node replacement. The two added unit tests directly prove those narrow cache contracts. They do not prove safe continuation into all physical-index consumers.
- Scope and clarity: the three-file change is focused, and the discriminator plus in-place
spliceare locally clear. The remaining correctness gap is a cross-consumer contract, not unrelated scope. - Concurrency: map/list operations are protected by one mutex;
splicepreserves iterators and copiedshared_ptrs remain alive. Construction outside the lock leaves the already-reported mixed physical/constant lost-update schedule; no fourth distinct race survived review. - Lifecycle and static initialization: the non-null constant is the immutable commit TSO for one rowset/segment cache identity. Publish has only a pre-visible
-1to final transition, and compaction/schema-change outputs use separate rowset-scoped caches. No cycle, release, destructor, or cross-TU initialization problem was found. - Configuration: no setting is added. The mutable capacity controls ordinary LRU eviction, which is why the existing eviction thread remains relevant; general runtime-shrink behavior predates this PR.
- Compatibility and rolling upgrade: no wire, RPC, storage-format, persisted-state, or FE/BE-variable change is introduced. The virtual discriminator is internal to an already-polymorphic BE hierarchy.
- Parallel and special paths: row production and segment-zone-map operations dispatch safely to constant implementations, while page-zone-map and Bloom paths decline pruning conservatively. The non-virtual physical
new_index_iteratorboundary remains unsafe when it receives the cached constant; eviction or the mixed-miss race can instead restore the placeholder's physical reader/index. These are the existing fenced P1s, not new comments. - Conditional checks: the
const_value/is_constant()condition is sufficient for a resident sequential entry, but cache residency and arrival order cannot encode the schema-level placeholder invariant required by later bare consumers. - Test coverage and results: the new unit tests cover sequential replacement, constant zone-map/type behavior, one-node LRU accounting, and eviction ordering. They do not cover the already-requested indexed continuation, barrier-controlled mixed miss, or low-capacity end-to-end sequence. Per the review contract, no local build or test was run. At final refresh, GitHub reported compile, BE UT, P0/non-concurrent/external/cloud regression, coverage, formatter, and style checks passing; those are CI claims, not independent validation.
- Observability: entry-count and eviction accounting remain balanced. A proposed hit/miss telemetry concern was dismissed after repository-wide search found no consumer or documented contract stronger than key-residency lookup semantics. No new required log or metric was identified.
- Transactions, persistence, writes, and crash safety: the PR changes read-side reader selection only; no EditLog, publish metadata, transaction atomicity, delete bitmap, or data-write path is modified.
- Performance and memory: lookup/replacement remain O(1), the virtual check is outside row-hot loops, and ownership remains
shared_ptr-safe. No material allocation, leak, or hot-path regression was found. - User focus: no additional review focus was supplied.
Review completion: converged after two bounded rounds. All Round 2 normal and risk-focused reviews returned NO_NEW_VALUABLE_FINDINGS; every candidate is accepted, duplicate-fenced, or dismissed with concrete evidence. The new inline set is empty solely because the blocking correctness issues are already posted above.
|
Agreed there is no new inline finding here; the three P1s it re-raises (r4017097815, r4025665398, r4032777651) are the same placeholder-index exposure, and I want to state how it is fenced rather than leave the deferral implicit. This PR is not meant to merge independently. It is ordered behind #67995, which moves placeholder handling into The reason the guard is not in this diff is the same evidence those three findings provide. A reader-type check cannot carry the invariant: eviction (r4032777651) and a concurrent bare miss (r4025665398) both rebuild a physical reader for a placeholder column, and the bare index path passes no constant, so the check is bypassed. That is why I removed the So this PR stays scoped to the cache contract: a request carrying a constant is not dropped on a hit, and the entry is replaced in place. That part its two unit tests cover on their own. #67995 lands first, then this one. |
What problem does this PR solve?
Issue Number: close #67994
Problem Summary:
__DORIS_COMMIT_TSO_COL__stores a0placeholder on disk in a single-version segment, and the real value reaches readers by building aConstantColumnReaderinstead of the on-disk one. That substitution goes through the per-segment reader cache, whose lookup drops the requested constant:So whichever caller populates the entry first decides what every later caller gets. The expression zone-map builders request a reader without a constant (
be/src/storage/segment/segment.cpp:129-130,be/src/storage/segment/segment_iterator.cpp:3414-3415) and the segment-level one runs insideSegment::new_iterator(:485-488), before theSegmentIteratorand therefore before anySegment::new_column_iteratorcall that would have installed the constant. Once the on-disk reader is cached,ConstantColumnReader::new_iteratoris never reached and reads of that column return0as row data, not only as a zone map, until the entry or the segment is evicted.That the mechanism is real is already recorded in the tree:
segment_zone_maps_can_answer_aggwalks every column with a bare request and skips exactly one ordinal, with the reason spelled out (be/src/storage/segment/segment.cpp:148-160): "Creating it here without one would cache a reader that hands every later read the on-disk placeholder instead."This makes a request that carries a constant authoritative. On a hit, the cached reader is returned only when the caller asked for no constant, or when the cached reader is already a constant one; otherwise the constant reader is built and replaces the entry, so later callers that cannot supply the constant stop seeing the placeholder too.
ColumnReader::is_constant()is the predicate the cache needs to tell the two apart.Replacing an entry required fixing the insert path.
_insert_locked_nocheckpushed a second LRU node for an existing key and overwrote only the map iterator, leaving the first node reachable through the list but not the map; eviction erases_cache_map[tail->key], so when that stale node reached the tail it erased the live entry of the same key while its node stayed in the list. It now updates the existing node in place.Nothing here changes what the zone-map builders themselves request. A cache-level fix cannot repair an evaluation that already consumed the placeholder summary, so the builders still have to ask for the constant; that is #67995.
The concurrent case is deliberately left as last-writer-wins: two callers that both miss can both insert, and if the bare one lands last the entry holds the on-disk reader. The next request carrying a constant then falls through and replaces it, and the row-read path always carries one, so the state self-heals rather than needing a tie-break rule. I could not write a test that distinguishes a tie-break rule from its absence, so I did not add one.
Release note
Fixed a bug where a column whose stored value is a placeholder could be read back as that placeholder after another caller warmed the segment's column reader cache.
Check List (For Author)
ConstValueIsNotDroppedOnCacheHitrequests a reader without a constant, then with one, and asserts the second is a constant reader whose segment zone map is the degenerate non-null summary of that value; then that a following bare request gets the same constant reader, that the cache still holds one entry, and that asking again with a constant is a plain hit. Iterator output has its own coverage inconstant_column_iterator_test.cpp.SameKeyReplacementDoesNotLeaveAStaleLruNodereplaces uid 1's entry, fills the cache past its capacity, and asserts uid 1 is gone from the reported readers and that the count matches the capacity.Both were checked by mutation: serving the cached reader unconditionally makes the first test fail on
is_constant(), and restoring the append-only insert makes the second fail on both assertions (uid 1 still reported, four entries instead of three).The reader-side handling of placeholder columns reaching index or zone-map pruning is deliberately not in this PR. That decision cannot rest on the reader's dynamic type or on which reader is cached: eviction, and concurrent physical/constant misses, both rebuild a physical reader for a column whose stored value is a placeholder. It has to be derived from the schema, which is #67995. This PR is scoped to the cache contract: a request carrying a constant is not dropped on a hit.
Behavior changed:
A request carrying a constant now returns a constant reader even when the column is already cached, and the cached entry is replaced.
Does this need documentation?