deepthi912 commented on code in PR #19596:
URL: https://github.com/apache/pinot/pull/19596#discussion_r4127328488


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManager.java:
##########
@@ -426,15 +434,15 @@ protected GenericRow doUpdateRecord(GenericRow record, 
RecordInfo recordInfo) {
           if (!recordInfo.isDeleteRecord()
               && 
recordInfo.getComparisonValue().compareTo(recordLocation.getComparisonValue()) 
>= 0) {
             IndexSegment currentSegment = recordLocation.getSegment();
-            ThreadSafeMutableRoaringBitmap currentQueryableDocIds = 
currentSegment.getQueryableDocIds();
             int currentDocId = recordLocation.getDocId();
-            if (currentQueryableDocIds == null || 
currentQueryableDocIds.contains(currentDocId)) {
+            // Read lock: currentSegment cannot be destroyed while LazyRow 
reads its columns. A consuming segment needs
+            // no lock: it is destroyed only after 
replaceSegment()/removeSegment() has moved or dropped every location
+            // pointing at it, and those run under the same per-key compute as 
this read.
+            if (tryAcquireSegmentReadLock(currentSegment)) {

Review Comment:
   Good call-out — this is the right thing to validate. Status, including what 
I don't yet have.
   
   **Structural point.** Each partition has one consumer thread and 
`_destroyLock` is per segment, so on the steady-state ingestion path the read 
lock is uncontended. The contention that does exist is with `destroy()`: it's a 
*non-fair* `ReentrantReadWriteLock`, so once a `destroy()` writer is queued, 
`readerShouldBlock()` parks new readers — and the consumer is inside a 
`computeIfPresent` bin lock at that moment. That's the case worth measuring, 
and my current harness does not produce it.
   
   **CPU — measured, but not yet conclusive.** JMH over a real 200K-row 
segment, comparing the previous-row read with and without the guard, varying 
columns carried forward (3 forks, 30 measurement iterations):
   
   | columns | unguarded | guarded | delta |
   |---|---|---|---|
   | 1 | 138.211 ± 1.209 | 140.013 ± 0.947 | +1.8 ns (+1.3%) |
   | 4 | 497.167 ± 19.133 | 465.753 ± 3.257 | −31.4 ns |
   | 8 | 893.593 ± 6.880 | 925.437 ± 18.095 | +31.8 ns (+3.6%) |
   
   The guard in isolation measures ~16.2 ns.
   
   I'm not drawing a conclusion from this. The 4-column row shows the guarded 
path as *faster*, which is impossible — the delta (~2–3% of the operation) is 
below the harness's resolution, and more forks only tightened the bars around 
noise. The harness also has fidelity gaps pulling both ways: it calls 
`ImmutableSegmentImpl#tryAcquireReadLock()` directly rather than through 
`tryAcquireSegmentReadLock(IndexSegment)`, missing the bimorphic `instanceof` 
dispatch (under-reports); and its denominator omits 
`PartialUpsertHandler.merge(...)`, the `queryableDocIds` check, the enclosing 
CHM bin lock, and null handling (over-reports). No partition manager is in the 
loop at all.
   
   The one point that reproduced across runs is 1 column: **+1.8 to +2.3 ns**, 
consistently positive and outside the error — about 8× below the guard's 
standalone cost, which would suggest the CAS largely overlaps with the 
column-read work. I want a correct harness before claiming that.
   
   **Memory.** One `ReentrantReadWriteLock` per `ImmutableSegmentImpl`, 
measured at **121 bytes/instance** (2M allocations, compressed oops, G1). At 
10K segments that's **~1.2 MB**, against segments that are megabytes each — 
under 0.01% of segment footprint. The `volatile boolean _destroyed` fits 
existing padding. The only part that grows is RRWL's `ThreadLocalHoldCounter`: 
one `ThreadLocalMap` entry per acquiring thread per lock. Acquirers are the 
partition's consumer thread and the segment-replace thread — query threads 
never take it — so it's bounded by segments-per-partition, not thread count.
   
   If footprint matters more than I think, the lighter shape is an 
`AtomicInteger` reader count with a destroyed sentinel: one int field, no lock 
object graph, no ThreadLocal. I don't think 1.2 MB justifies the extra 
complexity.
   
   **Next.** Measure the guard and the merge as separate terms and report the 
ratio, rather than subtracting two large noisy numbers; add a proper baseline 
arm; route through the production helper; and add a contended arm with a 
background `destroy()` writer. I'll post numbers when they're defensible.
   



-- 
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