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


##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java:
##########
@@ -122,6 +126,123 @@ protected void processTable(String tableNameWithType) {
 
     // Delete segments based on segment lineage and clean up segment lineage 
metadata.
     manageSegmentLineageCleanupForTable(tableConfig);
+
+    // Re-read active segments after time and lineage cleanup so 
already-deleted bytes do not cause extra eviction.
+    manageSizeBasedRetention(tableConfig);
+  }
+
+  /// Enforces the optional compressed-segment size limit independently of 
time retention. The limit counts one
+  /// copy of every active completed segment, including segments protected by 
lineage or realtime recovery. Unknown
+  /// sizes or missing metadata disable this pass; consuming segments are 
excluded from both accounting and eviction.
+  @VisibleForTesting
+  protected void manageSizeBasedRetention(TableConfig tableConfig) {
+    String retentionSize = 
tableConfig.getValidationConfig().getRetentionSize();
+    if (retentionSize == null) {
+      return;
+    }
+    String tableNameWithType = tableConfig.getTableName();
+    if (tableConfig.getTableType() == TableType.OFFLINE
+        && 
!"APPEND".equalsIgnoreCase(IngestionConfigUtils.getBatchSegmentIngestionType(tableConfig)))
 {
+      return;
+    }
+    long retentionSizeBytes;
+    try {
+      retentionSizeBytes = DataSizeUtils.toBytes(retentionSize);
+      Preconditions.checkArgument(retentionSizeBytes > 0, "Retention size must 
be positive");
+    } catch (Exception e) {
+      LOGGER.warn("Invalid retention size: {} for table: {}, skip size 
retention", retentionSize, tableNameWithType);
+      return;
+    }
+
+    Set<String> activeSegments = new 
HashSet<>(_pinotHelixResourceManager.getSegmentsFor(tableNameWithType, false));

Review Comment:
   Fixed in 
[2e170fcecf](https://github.com/apache/pinot/commit/2e170fcecfd32620a6ae093e7d5ca705571452f1).
 Size accounting now calls `getSegmentsFor(tableNameWithType, true)`, excluding 
COMPLETED replacements awaiting lineage cleanup. Any IN_PROGRESS lineage entry 
postpones the entire size pass, regardless of the lineage-exclusive deletion 
setting. Regression tests verify that replaced sources (including sources with 
unknown size) do not inflate the cap and that in-progress replacement cannot 
evict unrelated eligible live data.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java:
##########
@@ -122,6 +126,123 @@ protected void processTable(String tableNameWithType) {
 
     // Delete segments based on segment lineage and clean up segment lineage 
metadata.
     manageSegmentLineageCleanupForTable(tableConfig);
+
+    // Re-read active segments after time and lineage cleanup so 
already-deleted bytes do not cause extra eviction.
+    manageSizeBasedRetention(tableConfig);
+  }
+
+  /// Enforces the optional compressed-segment size limit independently of 
time retention. The limit counts one
+  /// copy of every active completed segment, including segments protected by 
lineage or realtime recovery. Unknown
+  /// sizes or missing metadata disable this pass; consuming segments are 
excluded from both accounting and eviction.
+  @VisibleForTesting
+  protected void manageSizeBasedRetention(TableConfig tableConfig) {
+    String retentionSize = 
tableConfig.getValidationConfig().getRetentionSize();
+    if (retentionSize == null) {
+      return;
+    }
+    String tableNameWithType = tableConfig.getTableName();
+    if (tableConfig.getTableType() == TableType.OFFLINE
+        && 
!"APPEND".equalsIgnoreCase(IngestionConfigUtils.getBatchSegmentIngestionType(tableConfig)))
 {
+      return;
+    }
+    long retentionSizeBytes;
+    try {
+      retentionSizeBytes = DataSizeUtils.toBytes(retentionSize);
+      Preconditions.checkArgument(retentionSizeBytes > 0, "Retention size must 
be positive");
+    } catch (Exception e) {
+      LOGGER.warn("Invalid retention size: {} for table: {}, skip size 
retention", retentionSize, tableNameWithType);
+      return;
+    }
+
+    Set<String> activeSegments = new 
HashSet<>(_pinotHelixResourceManager.getSegmentsFor(tableNameWithType, false));
+    if (activeSegments.isEmpty()) {
+      return;
+    }
+    List<SegmentZKMetadata> metadataList = 
_pinotHelixResourceManager.getSegmentsZKMetadata(tableNameWithType);
+    Set<String> segmentsWithMetadata = new HashSet<>();
+    List<SegmentZKMetadata> completedSegments = new ArrayList<>();
+    Map<String, Long> completedSegmentSizes = new HashMap<>();
+    boolean realtime = tableConfig.getTableType() == TableType.REALTIME;
+    BigInteger retainedBytes = BigInteger.ZERO;
+    for (SegmentZKMetadata metadata : metadataList) {
+      String segmentName = metadata.getSegmentName();
+      if (!activeSegments.contains(segmentName)) {
+        continue;
+      }
+      segmentsWithMetadata.add(segmentName);
+      if (realtime && !metadata.getStatus().isCompleted()) {
+        continue;
+      }
+      long size = getSizeRetentionSegmentSize(tableNameWithType, metadata);
+      if (size < 0) {
+        LOGGER.warn("Unknown size for segment: {} in table: {}, skip size 
retention", segmentName, tableNameWithType);
+        return;
+      }
+      // Avoid overflowing the table total even if individual segment sizes 
are valid longs.
+      retainedBytes = retainedBytes.add(BigInteger.valueOf(size));
+      completedSegments.add(metadata);
+      completedSegmentSizes.put(segmentName, size);
+    }
+    if (!segmentsWithMetadata.containsAll(activeSegments)) {
+      LOGGER.warn("Missing active segment metadata for table: {}, skip size 
retention", tableNameWithType);
+      return;
+    }
+    BigInteger limit = BigInteger.valueOf(retentionSizeBytes);
+    if (retainedBytes.compareTo(limit) <= 0) {
+      return;
+    }
+
+    List<String> candidateNames = completedSegments.stream()
+        .filter(metadata -> getSizeRetentionTimestamp(metadata) >= 0)
+        .map(SegmentZKMetadata::getSegmentName)
+        .collect(Collectors.toCollection(ArrayList::new));
+    if (realtime) {
+      
candidateNames.removeAll(_pinotHelixResourceManager.getLastLLCCompletedSegments(metadataList));
+    }
+    // Protect lineage before selecting victims: protected bytes still count 
toward the limit.
+    removeLineageLockedSegments(tableNameWithType, candidateNames);
+    Set<String> candidates = new HashSet<>(candidateNames);
+    completedSegments.removeIf(metadata -> 
!candidates.contains(metadata.getSegmentName()));
+    
completedSegments.sort(Comparator.comparingLong(RetentionManager::getSizeRetentionTimestamp)
+        .thenComparing(SegmentZKMetadata::getSegmentName));
+
+    List<String> segmentsToDelete = new ArrayList<>();
+    for (SegmentZKMetadata metadata : completedSegments) {
+      if (retainedBytes.compareTo(limit) <= 0) {
+        break;
+      }
+      long size = completedSegmentSizes.get(metadata.getSegmentName());
+      if (size == 0) {
+        continue;
+      }
+      segmentsToDelete.add(metadata.getSegmentName());
+      retainedBytes = retainedBytes.subtract(BigInteger.valueOf(size));
+    }
+    if (!segmentsToDelete.isEmpty()) {
+      LOGGER.info("Deleting {} oldest segments from table: {} for retention 
size: {}", segmentsToDelete.size(),
+          tableNameWithType, retentionSize);
+      // The public deletion path rechecks live lineage in case it changed 
after candidate selection.
+      _pinotHelixResourceManager.deleteSegments(tableNameWithType, 
segmentsToDelete);

Review Comment:
   Fixed in 
[2e170fcecf](https://github.com/apache/pinot/commit/2e170fcecfd32620a6ae093e7d5ca705571452f1).
 The size pass checks `PinotResourceManagerResponse.isSuccessful()` before 
accepting projected reductions or logging successful deletion. A rejected or 
null batch response logs the failure with the original retained bytes and cap, 
leaves `SIZE_RETENTION_BLOCKED` at 1, and retries on the next cycle. Regression 
tests cover lineage rejection/null response and successful retry clearing the 
gauge.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java:
##########
@@ -122,6 +126,123 @@ protected void processTable(String tableNameWithType) {
 
     // Delete segments based on segment lineage and clean up segment lineage 
metadata.
     manageSegmentLineageCleanupForTable(tableConfig);
+
+    // Re-read active segments after time and lineage cleanup so 
already-deleted bytes do not cause extra eviction.
+    manageSizeBasedRetention(tableConfig);
+  }
+
+  /// Enforces the optional compressed-segment size limit independently of 
time retention. The limit counts one
+  /// copy of every active completed segment, including segments protected by 
lineage or realtime recovery. Unknown
+  /// sizes or missing metadata disable this pass; consuming segments are 
excluded from both accounting and eviction.
+  @VisibleForTesting
+  protected void manageSizeBasedRetention(TableConfig tableConfig) {
+    String retentionSize = 
tableConfig.getValidationConfig().getRetentionSize();
+    if (retentionSize == null) {
+      return;
+    }
+    String tableNameWithType = tableConfig.getTableName();
+    if (tableConfig.getTableType() == TableType.OFFLINE
+        && 
!"APPEND".equalsIgnoreCase(IngestionConfigUtils.getBatchSegmentIngestionType(tableConfig)))
 {
+      return;
+    }
+    long retentionSizeBytes;
+    try {
+      retentionSizeBytes = DataSizeUtils.toBytes(retentionSize);
+      Preconditions.checkArgument(retentionSizeBytes > 0, "Retention size must 
be positive");
+    } catch (Exception e) {
+      LOGGER.warn("Invalid retention size: {} for table: {}, skip size 
retention", retentionSize, tableNameWithType);
+      return;
+    }
+
+    Set<String> activeSegments = new 
HashSet<>(_pinotHelixResourceManager.getSegmentsFor(tableNameWithType, false));
+    if (activeSegments.isEmpty()) {
+      return;
+    }
+    List<SegmentZKMetadata> metadataList = 
_pinotHelixResourceManager.getSegmentsZKMetadata(tableNameWithType);
+    Set<String> segmentsWithMetadata = new HashSet<>();
+    List<SegmentZKMetadata> completedSegments = new ArrayList<>();
+    Map<String, Long> completedSegmentSizes = new HashMap<>();
+    boolean realtime = tableConfig.getTableType() == TableType.REALTIME;
+    BigInteger retainedBytes = BigInteger.ZERO;
+    for (SegmentZKMetadata metadata : metadataList) {
+      String segmentName = metadata.getSegmentName();
+      if (!activeSegments.contains(segmentName)) {
+        continue;
+      }
+      segmentsWithMetadata.add(segmentName);
+      if (realtime && !metadata.getStatus().isCompleted()) {
+        continue;
+      }
+      long size = getSizeRetentionSegmentSize(tableNameWithType, metadata);
+      if (size < 0) {
+        LOGGER.warn("Unknown size for segment: {} in table: {}, skip size 
retention", segmentName, tableNameWithType);
+        return;
+      }
+      // Avoid overflowing the table total even if individual segment sizes 
are valid longs.
+      retainedBytes = retainedBytes.add(BigInteger.valueOf(size));
+      completedSegments.add(metadata);
+      completedSegmentSizes.put(segmentName, size);
+    }
+    if (!segmentsWithMetadata.containsAll(activeSegments)) {
+      LOGGER.warn("Missing active segment metadata for table: {}, skip size 
retention", tableNameWithType);
+      return;
+    }
+    BigInteger limit = BigInteger.valueOf(retentionSizeBytes);
+    if (retainedBytes.compareTo(limit) <= 0) {
+      return;
+    }
+
+    List<String> candidateNames = completedSegments.stream()
+        .filter(metadata -> getSizeRetentionTimestamp(metadata) >= 0)
+        .map(SegmentZKMetadata::getSegmentName)
+        .collect(Collectors.toCollection(ArrayList::new));
+    if (realtime) {
+      
candidateNames.removeAll(_pinotHelixResourceManager.getLastLLCCompletedSegments(metadataList));
+    }
+    // Protect lineage before selecting victims: protected bytes still count 
toward the limit.
+    removeLineageLockedSegments(tableNameWithType, candidateNames);
+    Set<String> candidates = new HashSet<>(candidateNames);
+    completedSegments.removeIf(metadata -> 
!candidates.contains(metadata.getSegmentName()));
+    
completedSegments.sort(Comparator.comparingLong(RetentionManager::getSizeRetentionTimestamp)
+        .thenComparing(SegmentZKMetadata::getSegmentName));
+
+    List<String> segmentsToDelete = new ArrayList<>();

Review Comment:
   Fixed in 
[2e170fcecf](https://github.com/apache/pinot/commit/2e170fcecfd32620a6ae093e7d5ca705571452f1).
 OFFLINE size retention always preserves the newest dated segment, defined by 
the same end-time/creation-time/push-time fallback and segment-name tie-break 
used for eviction. This includes a single segment larger than the cap. If this 
floor prevents reaching the cap, retention warns and keeps the blocked gauge at 
1. Tests cover an oversized newest segment, the only remaining segment, 
timestamp fallbacks, and deterministic ties. The safeguard and best-effort 
limit are explicit in the configuration Javadoc and PR description.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java:
##########
@@ -122,6 +126,123 @@ protected void processTable(String tableNameWithType) {
 
     // Delete segments based on segment lineage and clean up segment lineage 
metadata.
     manageSegmentLineageCleanupForTable(tableConfig);
+
+    // Re-read active segments after time and lineage cleanup so 
already-deleted bytes do not cause extra eviction.
+    manageSizeBasedRetention(tableConfig);
+  }
+
+  /// Enforces the optional compressed-segment size limit independently of 
time retention. The limit counts one

Review Comment:
   Fixed in 
[2e170fcecf](https://github.com/apache/pinot/commit/2e170fcecfd32620a6ae093e7d5ca705571452f1).
 When hybrid retention is enabled and an OFFLINE counterpart exists, REALTIME 
size eviction requires a known segment end time strictly below the OFFLINE time 
boundary. Segments at/above the boundary and segments with unknown end time are 
preserved; an unavailable or invalid boundary postpones eviction. Both 
retention paths reuse the existing boundary resolver. Tests cover strict 
boundary equality, newer/unknown end times, missing/invalid boundaries, and 
ordinary behavior when the strategy is disabled or there is no OFFLINE 
counterpart.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java:
##########
@@ -122,6 +126,123 @@ protected void processTable(String tableNameWithType) {
 
     // Delete segments based on segment lineage and clean up segment lineage 
metadata.
     manageSegmentLineageCleanupForTable(tableConfig);
+
+    // Re-read active segments after time and lineage cleanup so 
already-deleted bytes do not cause extra eviction.
+    manageSizeBasedRetention(tableConfig);
+  }
+
+  /// Enforces the optional compressed-segment size limit independently of 
time retention. The limit counts one
+  /// copy of every active completed segment, including segments protected by 
lineage or realtime recovery. Unknown
+  /// sizes or missing metadata disable this pass; consuming segments are 
excluded from both accounting and eviction.
+  @VisibleForTesting
+  protected void manageSizeBasedRetention(TableConfig tableConfig) {
+    String retentionSize = 
tableConfig.getValidationConfig().getRetentionSize();
+    if (retentionSize == null) {
+      return;
+    }
+    String tableNameWithType = tableConfig.getTableName();
+    if (tableConfig.getTableType() == TableType.OFFLINE
+        && 
!"APPEND".equalsIgnoreCase(IngestionConfigUtils.getBatchSegmentIngestionType(tableConfig)))
 {
+      return;
+    }
+    long retentionSizeBytes;
+    try {
+      retentionSizeBytes = DataSizeUtils.toBytes(retentionSize);
+      Preconditions.checkArgument(retentionSizeBytes > 0, "Retention size must 
be positive");
+    } catch (Exception e) {
+      LOGGER.warn("Invalid retention size: {} for table: {}, skip size 
retention", retentionSize, tableNameWithType);
+      return;
+    }
+
+    Set<String> activeSegments = new 
HashSet<>(_pinotHelixResourceManager.getSegmentsFor(tableNameWithType, false));
+    if (activeSegments.isEmpty()) {
+      return;
+    }
+    List<SegmentZKMetadata> metadataList = 
_pinotHelixResourceManager.getSegmentsZKMetadata(tableNameWithType);
+    Set<String> segmentsWithMetadata = new HashSet<>();
+    List<SegmentZKMetadata> completedSegments = new ArrayList<>();
+    Map<String, Long> completedSegmentSizes = new HashMap<>();
+    boolean realtime = tableConfig.getTableType() == TableType.REALTIME;
+    BigInteger retainedBytes = BigInteger.ZERO;
+    for (SegmentZKMetadata metadata : metadataList) {
+      String segmentName = metadata.getSegmentName();
+      if (!activeSegments.contains(segmentName)) {
+        continue;
+      }
+      segmentsWithMetadata.add(segmentName);
+      if (realtime && !metadata.getStatus().isCompleted()) {
+        continue;
+      }
+      long size = getSizeRetentionSegmentSize(tableNameWithType, metadata);
+      if (size < 0) {
+        LOGGER.warn("Unknown size for segment: {} in table: {}, skip size 
retention", segmentName, tableNameWithType);
+        return;
+      }
+      // Avoid overflowing the table total even if individual segment sizes 
are valid longs.
+      retainedBytes = retainedBytes.add(BigInteger.valueOf(size));
+      completedSegments.add(metadata);
+      completedSegmentSizes.put(segmentName, size);
+    }
+    if (!segmentsWithMetadata.containsAll(activeSegments)) {
+      LOGGER.warn("Missing active segment metadata for table: {}, skip size 
retention", tableNameWithType);
+      return;
+    }
+    BigInteger limit = BigInteger.valueOf(retentionSizeBytes);
+    if (retainedBytes.compareTo(limit) <= 0) {
+      return;
+    }
+
+    List<String> candidateNames = completedSegments.stream()
+        .filter(metadata -> getSizeRetentionTimestamp(metadata) >= 0)
+        .map(SegmentZKMetadata::getSegmentName)
+        .collect(Collectors.toCollection(ArrayList::new));
+    if (realtime) {
+      
candidateNames.removeAll(_pinotHelixResourceManager.getLastLLCCompletedSegments(metadataList));
+    }
+    // Protect lineage before selecting victims: protected bytes still count 
toward the limit.
+    removeLineageLockedSegments(tableNameWithType, candidateNames);
+    Set<String> candidates = new HashSet<>(candidateNames);
+    completedSegments.removeIf(metadata -> 
!candidates.contains(metadata.getSegmentName()));
+    
completedSegments.sort(Comparator.comparingLong(RetentionManager::getSizeRetentionTimestamp)
+        .thenComparing(SegmentZKMetadata::getSegmentName));
+
+    List<String> segmentsToDelete = new ArrayList<>();
+    for (SegmentZKMetadata metadata : completedSegments) {
+      if (retainedBytes.compareTo(limit) <= 0) {
+        break;
+      }
+      long size = completedSegmentSizes.get(metadata.getSegmentName());
+      if (size == 0) {
+        continue;
+      }
+      segmentsToDelete.add(metadata.getSegmentName());
+      retainedBytes = retainedBytes.subtract(BigInteger.valueOf(size));
+    }
+    if (!segmentsToDelete.isEmpty()) {
+      LOGGER.info("Deleting {} oldest segments from table: {} for retention 
size: {}", segmentsToDelete.size(),
+          tableNameWithType, retentionSize);
+      // The public deletion path rechecks live lineage in case it changed 
after candidate selection.
+      _pinotHelixResourceManager.deleteSegments(tableNameWithType, 
segmentsToDelete);
+    }
+    if (retainedBytes.compareTo(limit) > 0) {
+      LOGGER.warn("Table: {} remains above retention size: {} due to protected 
or undated segments",

Review Comment:
   Fixed in 
[2e170fcecf](https://github.com/apache/pinot/commit/2e170fcecfd32620a6ae093e7d5ca705571452f1).
 Added table-scoped `sizeRetentionBlocked` 
(`ControllerGauge.SIZE_RETENTION_BLOCKED`): 1 for unsafe skips, failed 
deletion, or an unmet cap; 0 for a disabled/unsupported policy, an 
empty/within-cap table, or successful eviction within the cap. The pass 
initializes conservatively so read exceptions cannot leave a stale healthy 
status, clears on recovery, and removes the gauge when table leadership is lost 
or the table is deleted. Regression tests cover blocked states, unknown-size 
recovery, deletion retry, disabling the policy, empty tables, and leadership 
cleanup.



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