xiangfu0 commented on code in PR #20161:
URL: https://github.com/apache/pinot/pull/20161#discussion_r4477615030


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManager.java:
##########
@@ -58,6 +71,85 @@ public class ConcurrentMapPartitionUpsertMetadataManager 
extends BasePartitionUp
 
   public ConcurrentMapPartitionUpsertMetadataManager(String tableNameWithType, 
int partitionId, UpsertContext context) {
     super(tableNameWithType, partitionId, context);
+    _recoveringLegacySnapshots = _metadataTTL > 0 && !hasVersionedWatermark();
+    _initialSegmentsLoaded = _metadataTTL <= 0;
+    if (_recoveringLegacySnapshots) {
+      _logger.warn("Rebuilding legacy snapshot ownership before trusting the 
TTL watermark");
+    }
+  }
+
+  private boolean hasVersionedWatermark() {
+    try {
+      byte[] bytes = Files.readAllBytes(getWatermarkFile().toPath());
+      return bytes.length == 2 * Long.BYTES
+          && ByteBuffer.wrap(bytes).getLong(Double.BYTES) == 
SNAPSHOT_WATERMARK_VERSION;
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
+  @Override
+  protected boolean canSkipSegmentOutOfTTL(double maxComparisonValue) {
+    return !_recoveringLegacySnapshots && 
super.canSkipSegmentOutOfTTL(maxComparisonValue);
+  }
+
+  @Override
+  protected boolean isOutOfMetadataTTL(IndexSegment segment) {
+    // Recovery retains expired keys, so offload must remove them before their 
segment is destroyed.
+    return !_recoveringLegacySnapshots && _initialSegmentsLoaded && 
super.isOutOfMetadataTTL(segment);
+  }
+
+  /// Wait for all assigned ONLINE segments, including segments without 
snapshots. The first consuming segment
+  /// alone is insufficient for full upsert: it can arrive before the 
historical segments.
+  protected boolean isInitialSegmentLoadComplete() {
+    TableDataManager tableDataManager = _context.getTableDataManager();
+    if (tableDataManager == null) {
+      return false;
+    }
+    HelixManager helixManager = tableDataManager.getHelixManager();
+    if (helixManager == null || 
helixManager.getHelixDataAccessor().getProperty(
+        
helixManager.getHelixDataAccessor().keyBuilder().idealStates(_tableNameWithType))
 == null) {
+      return false;
+    }
+    return TableStateUtils.isAllSegmentsLoaded(helixManager, 
_tableNameWithType);
+  }
+
+  @Override
+  protected boolean isReadyToTakeSnapshot() {
+    if (_initialSegmentsLoaded) {
+      return true;
+    }
+    try {
+      if (!isInitialSegmentLoadComplete()) {
+        _logger.info("Defer snapshot and TTL cleanup until all assigned 
segments are loaded");
+        return false;
+      }
+      _initialSegmentsLoaded = true;

Review Comment:
   **MAJOR:** after a restart, this latch can stay unset indefinitely. Until it 
sets, there are no snapshots and no TTL or deleted-key cleanup.
   
   **Why it keeps failing.** The latch is only checked when a consumer starts 
(`takeSnapshot` + `removeExpiredPrimaryKeys`). It needs every IS-ONLINE segment 
of the table to also be ONLINE in the current state at that moment. On 
non-committer replicas, segment N's CONSUMING→ONLINE handler releases the 
consumer semaphore before download/build. `ALLOW_ALWAYS` is the default for 
full upsert, and under DISALLOW policies the release happens in `doOffload`, 
still inside the handler. Helix writes CS=ONLINE only after the handler 
returns, so consumer N+1 always sees N as IS=ONLINE/CS=CONSUMING. If the first 
check after restart misses (the case this PR targets), every later check misses 
too.
   
   **Repro.** Temporary test: real `TableStateUtils` with mocked Helix, 
versioned watermark, a restart, then 3 commits.
   - This PR: `lastSnapshotPass=null` throughout, keys grow 2→5, persisted 
watermark stuck at 290.
   - Merge-base: a pass at every consumer start, keys stay at 2, watermark goes 
290→400→500.
   
   This hits trusted tables too, not only legacy ones.
   
   **Possible fix.** Latch against the ONLINE set captured at startup (ignore 
segments that turn ONLINE later or are mid CONSUMING→ONLINE), or use a periodic 
table-level readiness check. Scope the gate to legacy recovery only.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManager.java:
##########
@@ -58,6 +71,85 @@ public class ConcurrentMapPartitionUpsertMetadataManager 
extends BasePartitionUp
 
   public ConcurrentMapPartitionUpsertMetadataManager(String tableNameWithType, 
int partitionId, UpsertContext context) {
     super(tableNameWithType, partitionId, context);
+    _recoveringLegacySnapshots = _metadataTTL > 0 && !hasVersionedWatermark();
+    _initialSegmentsLoaded = _metadataTTL <= 0;
+    if (_recoveringLegacySnapshots) {
+      _logger.warn("Rebuilding legacy snapshot ownership before trusting the 
TTL watermark");
+    }
+  }
+
+  private boolean hasVersionedWatermark() {
+    try {
+      byte[] bytes = Files.readAllBytes(getWatermarkFile().toPath());
+      return bytes.length == 2 * Long.BYTES
+          && ByteBuffer.wrap(bytes).getLong(Double.BYTES) == 
SNAPSHOT_WATERMARK_VERSION;
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
+  @Override
+  protected boolean canSkipSegmentOutOfTTL(double maxComparisonValue) {
+    return !_recoveringLegacySnapshots && 
super.canSkipSegmentOutOfTTL(maxComparisonValue);
+  }
+
+  @Override
+  protected boolean isOutOfMetadataTTL(IndexSegment segment) {
+    // Recovery retains expired keys, so offload must remove them before their 
segment is destroyed.
+    return !_recoveringLegacySnapshots && _initialSegmentsLoaded && 
super.isOutOfMetadataTTL(segment);
+  }
+
+  /// Wait for all assigned ONLINE segments, including segments without 
snapshots. The first consuming segment
+  /// alone is insufficient for full upsert: it can arrive before the 
historical segments.
+  protected boolean isInitialSegmentLoadComplete() {
+    TableDataManager tableDataManager = _context.getTableDataManager();
+    if (tableDataManager == null) {
+      return false;
+    }
+    HelixManager helixManager = tableDataManager.getHelixManager();
+    if (helixManager == null || 
helixManager.getHelixDataAccessor().getProperty(
+        
helixManager.getHelixDataAccessor().keyBuilder().idealStates(_tableNameWithType))
 == null) {
+      return false;
+    }
+    return TableStateUtils.isAllSegmentsLoaded(helixManager, 
_tableNameWithType);
+  }
+
+  @Override
+  protected boolean isReadyToTakeSnapshot() {
+    if (_initialSegmentsLoaded) {
+      return true;
+    }
+    try {
+      if (!isInitialSegmentLoadComplete()) {
+        _logger.info("Defer snapshot and TTL cleanup until all assigned 
segments are loaded");
+        return false;
+      }
+      _initialSegmentsLoaded = true;
+      return true;
+    } catch (Exception e) {
+      _logger.warn("Cannot verify that initial segment loading is complete, 
deferring snapshot and TTL cleanup", e);
+      return false;
+    }
+  }
+
+  @Override
+  protected synchronized void doTakeSnapshot(@Nullable DocIdsSnapshot.Trigger 
trigger) {

Review Comment:
   Minor: `synchronized` holds the manager monitor for the entire snapshot 
pass. Meanwhile, `startOperation()` on Helix threads 
(add/replace/remove/preload for this partition) and `stop()` block until the 
pass ends, though the pass itself was designed to be non-blocking with 
`tryLock`. Snapshots already run serially on the consumer thread, so either 
drop the lock or narrow it to the versioned publish.



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