KKcorps opened a new pull request, #19504:
URL: https://github.com/apache/pinot/pull/19504

   ## TL;DR
   
   The "Found N primary keys not replaced" warning and the 
`upsertInconsistentRows` /
   `partialUpsertKeysNotReplaced` metrics are computed from a `validDocIds` 
bitmap cloned *before*
   segment replacement began, and never reconciled against the live state 
afterwards. Keys that
   ingestion correctly moved onto a newer segment mid-replacement get reported 
as not replaced. This
   counts at the point where the code already decides, per key, whether the key 
still belongs to the
   segment being removed.
   
   ## The problem
   
   `doAddOrReplaceSegment` takes `validDocIdsForOldSegment` as a snapshot of 
the old segment's valid
   docs. The replacement then runs, which takes time on a large segment. While 
it runs, ingestion keeps
   going: a new record for one of those primary keys updates the live 
record-location map and points
   the key at the newer consuming segment. That is correct upsert behaviour, 
and it is exactly what
   should happen.
   
   At the end, the code reports `validDocIdsForOldSegment.getCardinality()` — 
the size of the stale
   clone. Every key that moved during the window is counted as "not replaced" 
even though it was
   replaced properly.
   
   ```mermaid
   sequenceDiagram
     participant I as Ingestion
     participant R as Replacement
     participant M as Record location map
   
     R->>M: clone validDocIds of old segment
     Note over R: clone says keys {A, B} still valid
     I->>M: new record for key A
     M-->>I: A now points at the consuming segment
     Note over R: replacement finishes
     R->>R: report clone cardinality = 2
     Note over R: ❌ A is counted, but A was replaced correctly
   ```
   
   The consequence is not just a noisy log. `UpsertInconsistentReplicas` is 
built on these metrics and
   is muted across a large number of production environments because of this 
false positive, so a real
   divergence on those tables now goes unreported.
   
   ## The approach
   
   `removeSegment` already answers the question correctly. For each candidate 
key it does a
   `computeIfPresent` and only acts when `recordLocation.getSegment() == 
segment`, so a key ingestion
   moved away fails that check and is skipped. That per-key check is the 
authoritative answer to "was
   this key actually still owned by the old segment".
   
   1. Add 
`BasePartitionUpsertMetadataManager.removeSegmentAndGetNumKeysRemoved(IndexSegment,
      MutableRoaringBitmap)`, returning how many keys passed that ownership 
check.
   2. Default it to `removeSegment(...)` followed by the bitmap cardinality, so 
any metadata-manager
      implementation that does not override keeps its current behaviour exactly.
   3. Override it in both `ConcurrentMap` managers, counting inside the 
`computeIfPresent` where the
      ownership check happens.
   4. Report the warning and the metric from that count, and only when it is 
above zero.
   
   ## Key components
   
   | Class / file | Change |
   |---|---|
   | `BasePartitionUpsertMetadataManager` | New 
`removeSegmentAndGetNumKeysRemoved` with a compatible default; reporting moved 
to use its return value |
   | `ConcurrentMapPartitionUpsertMetadataManager` | Overrides it; 
`removeSegment(segment, iterator)` refactored to share one private body with an 
optional counter |
   | `ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletes` | Same, 
plus `doRemoveSegmentAndGetNumKeysRemoved`, since that path walks every doc 
rather than the valid ones |
   
   ## Flow after the change
   
   ```mermaid
   sequenceDiagram
     participant R as doAddOrReplaceSegment
     participant Rm as removeSegmentAndGetNumKeysRemoved
     participant M as Record location map
   
     R->>Rm: candidate keys from the (stale) clone
     loop per candidate key
       Rm->>M: computeIfPresent(key)
       alt still points at the old segment
         M-->>Rm: removed, count it
       else already moved by ingestion
         M-->>Rm: untouched, not counted
       end
     end
     Rm-->>R: numKeysStillNotReplaced
     alt count > 0
       R->>R: warn + updateInconsistentRowsMetric
     else count == 0
       R->>R: silent, the replacement was clean
     end
   ```
   
   ## Why the reporting moved after removal in the consistent-deletes manager
   
   
`ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletes.doRemoveSegment`
 deliberately walks
   *every* doc in the segment, not just the valid ones, so it can decrement 
`distinctSegmentCount` for
   every key that was ever there. That means the count only becomes knowable 
once the removal has run,
   so the warning and metric move below it. The 
`shouldRevertMetadataOnInconsistency` early return is
   unchanged, and the removal itself was already unconditional on that path.
   
   ## Behaviour changes
   
   - The warning and the metric no longer fire when every candidate key had 
already been replaced.
     Previously they fired with an inflated count. A table with a clean 
replacement goes from a
     spurious warning to silence.
   - When there is genuine inconsistency the count is now the real number of 
unreplaced keys, which
     will generally be smaller than what was reported before.
   - No config, no metric names, no wire format changed. `removeSegment` keeps 
its signature and
     behaviour for every existing caller.
   
   ## Performance considerations
   
   - No extra pass over anything. The counting is an increment inside the 
`computeIfPresent` the code
     already performs per key.
   - The private `removeSegment(segment, iterator, @Nullable int[] counter)` 
body is shared by the
     counting and non-counting paths, so the non-counting path is unchanged 
apart from a null check per
     key.
   - The removal was already happening on this path. Only the reporting moved.
   
   ## Testing
   
   | Test | Covers |
   |---|---|
   | 
`BasePartitionUpsertMetadataManagerTest.testReplaceSegmentDoesNotReportKeyMovedBeforeRemoval`
 | A key moved onto a newer segment during replacement is not reported |
   | 
`BasePartitionUpsertMetadataManagerTest.testReplaceSegmentReportsOnlyKeysStillOwnedAtRemoval`
 | A genuinely unreplaced key is still reported, with the right count |
   | 
`ConcurrentMapPartitionUpsertMetadataManagerTest.testRemoveSegmentCountsOnlyKeysStillOwnedBySegment`
 | The override counts only keys passing the ownership check |
   | 
`ConcurrentMapPartitionUpsertMetadataManagerForConsistentDeletesTest.testRemoveSegmentCountsOnlyKeysStillOwnedBySegment`
 | Same for the consistent-deletes path |
   
   Every test in `org.apache.pinot.segment.local.upsert` passes: 104 tests, 0 
failures. `spotless`,
   `checkstyle`, `license:format` and `license:check` are clean on 
`pinot-segment-local`.
   
   ## References
   
   - Postmortem: ZD#7481, RCA-299
   - StarTree's off-heap RocksDB metadata manager needs the same treatment on 
its async-removal path,
     where the count is only knowable during the background RocksDB scan. That 
is a separate change in
     the StarTree fork and does not affect this PR.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to