yashmayya commented on code in PR #19252:
URL: https://github.com/apache/pinot/pull/19252#discussion_r3799115547


##########
pinot-controller/src/test/java/org/apache/pinot/controller/helix/SegmentStatusCheckerTest.java:
##########
@@ -626,6 +671,198 @@ public void 
realtimeCommittingSegmentWithinGraceNotUnderReplicated() {
         ControllerGauge.SEGMENTS_WITH_LESS_REPLICAS), 0);
   }
 
+  /// When a segment's znode stat is unavailable, the grace window falls back 
to the metadata's creation time. A freshly
+  /// created CONSUMING segment (mtime == creation time) must therefore still 
be graced, so a lost stat cannot turn into
+  /// a false under-replication alert.
+  @Test
+  public void 
realtimeConsumingSegmentWithoutZNodeStatFallsBackToCreationTime() {
+    TableConfig tableConfig =
+        new 
TableConfigBuilder(TableType.REALTIME).setTableName(RAW_TABLE_NAME).setTimeColumnName("timeColumn")
+            .setNumReplicas(3).setStreamConfigs(getStreamConfigMap()).build();
+
+    String seg = new LLCSegmentName(RAW_TABLE_NAME, 1, 5, 
System.currentTimeMillis()).getSegmentName();
+    IdealState idealState = new IdealState(REALTIME_TABLE_NAME);
+    idealState.setPartitionState(seg, "pinot1", "CONSUMING");
+    idealState.setPartitionState(seg, "pinot2", "CONSUMING");
+    idealState.setPartitionState(seg, "pinot3", "CONSUMING");
+    idealState.setReplicas("3");
+    idealState.setRebalanceMode(IdealState.RebalanceMode.CUSTOMIZED);
+
+    // Just created: only 1 of 3 replicas has started consuming.
+    ExternalView externalView = new ExternalView(REALTIME_TABLE_NAME);
+    externalView.setState(seg, "pinot1", "CONSUMING");
+    externalView.setState(seg, "pinot2", "OFFLINE");
+    externalView.setState(seg, "pinot3", "OFFLINE");
+
+    PinotHelixResourceManager resourceManager = 
mock(PinotHelixResourceManager.class);
+    
when(resourceManager.getHelixInstanceConfig(any())).thenReturn(newQuerableInstanceConfig("any"));
+    
when(resourceManager.getTableConfig(REALTIME_TABLE_NAME)).thenReturn(tableConfig);
+    
when(resourceManager.getAllTables()).thenReturn(List.of(REALTIME_TABLE_NAME));
+    
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
+    
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
+    // Creation time is now, and no znode stat is available -> the fallback 
must keep the segment within the grace
+    // window
+    SegmentZKMetadata consumingSegmentZKMetadata = 
mockConsumingSegmentZKMetadata(System.currentTimeMillis());
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME, Map.of(seg, 
consumingSegmentZKMetadata));
+
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
+    when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
+    ZNRecord znRecord = new ZNRecord("0");
+    znRecord.setSimpleField(CommonConstants.Segment.Realtime.END_OFFSET, 
"10000");
+    when(propertyStore.get(anyString(), any(), anyInt())).thenReturn(znRecord);
+
+    // 1h grace window; the segment was just created, so it must be skipped 
and the table stays fully replicated.
+    runSegmentStatusChecker(resourceManager, 3600);
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
REALTIME_TABLE_NAME,
+        ControllerGauge.PERCENT_OF_REPLICAS), 100);
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
REALTIME_TABLE_NAME,
+        ControllerGauge.SEGMENTS_WITH_LESS_REPLICAS), 0);
+  }
+
+  /// The whole table's metadata and znode stats come back from batched reads, 
so each segment must be matched to its
+  /// own metadata (by name) and its own stat (by position) rather than to 
whichever entry happens to sit at its index.
+  /// Every segment is under-replicated and carries a distinct size, exactly 
one segment was pushed recently enough to
+  /// be graced, and the last segment is the only one with 4 replicas so it 
alone determines PERCENT_OF_REPLICAS.
+  /// Together the gauges pin that every segment was examined under its own 
name, that the grace window applied to
+  /// exactly the segment whose znode stat is recent, and that the sizes 
accumulate. One segment has no ZK metadata,
+  /// which shifts the alignment if the pairing gets it wrong.
+  @Test
+  public void segmentsStayAlignedWithTheirBatchedMetadata() {
+    TableConfig tableConfig =
+        new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME).setNumReplicas(2).build();
+
+    int numSegments = 7;
+    // Distinct positions, none of them the last segment, which carries its 
own marker below
+    int segmentWithoutZKMetadata = 3;
+    int recentlyPushedSegment = 1;
+    // The last segment is the marker that pins name-to-metadata pairing: it 
is the only one whose replica ratio is 1/4
+    // rather than 1/2, so PERCENT_OF_REPLICAS drops to 25 only if this exact 
segment was examined.
+    int lowReplicaSegment = numSegments - 1;
+    long oldPushTimeMs = 11111L;
+
+    IdealState idealState = new IdealState(OFFLINE_TABLE_NAME);
+    ExternalView externalView = new ExternalView(OFFLINE_TABLE_NAME);
+    Map<String, SegmentZKMetadata> segmentZKMetadataMap = new HashMap<>();
+    Map<String, Long> segmentZNodeMTimesMs = new HashMap<>();
+    long expectedTableCompressedSize = 0;
+    for (int i = 0; i < numSegments; i++) {
+      String segment = "myTable_" + i;
+      int numReplicas = i == lowReplicaSegment ? 4 : 2;
+      // Every segment is under-replicated, so any segment that is examined 
and not graced must be counted
+      for (int replica = 1; replica <= numReplicas; replica++) {
+        idealState.setPartitionState(segment, "pinot" + replica, "ONLINE");
+        externalView.setState(segment, "pinot" + replica, replica == 1 ? 
"ONLINE" : "OFFLINE");
+      }
+      if (i == segmentWithoutZKMetadata) {
+        continue;
+      }
+      // Distinct size per segment so that the total pins which metadata was 
attributed to which segment
+      long sizeInBytes = 1000L + i;
+      segmentZKMetadataMap.put(segment, 
mockPushedSegmentZKMetadata(sizeInBytes, oldPushTimeMs));
+      segmentZNodeMTimesMs.put(segment, i == recentlyPushedSegment ? 
System.currentTimeMillis() : oldPushTimeMs);
+      expectedTableCompressedSize += sizeInBytes;
+    }
+    idealState.setReplicas("2");
+    idealState.setRebalanceMode(IdealState.RebalanceMode.CUSTOMIZED);
+
+    PinotHelixResourceManager resourceManager = 
mock(PinotHelixResourceManager.class);
+    
when(resourceManager.getHelixInstanceConfig(any())).thenReturn(newQuerableInstanceConfig("any"));
+    
when(resourceManager.getAllTables()).thenReturn(List.of(OFFLINE_TABLE_NAME));
+    
when(resourceManager.getTableConfig(OFFLINE_TABLE_NAME)).thenReturn(tableConfig);
+    
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
+    
when(resourceManager.getTableExternalView(OFFLINE_TABLE_NAME)).thenReturn(externalView);
+    mockSegmentsZKMetadata(resourceManager, OFFLINE_TABLE_NAME, 
segmentZKMetadataMap, segmentZNodeMTimesMs);
+
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
+    when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
+
+    // 10min grace window, so only the recently pushed segment is skipped
+    runSegmentStatusChecker(resourceManager, 600, mock(TableSizeReader.class));
+
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
OFFLINE_TABLE_NAME,
+        ControllerGauge.SEGMENT_COUNT), numSegments);
+    // Every segment except the one without ZK metadata and the graced one 
must be counted. A segment paired with the
+    // wrong metadata or the wrong znode stat drops out of, or into, this 
count.
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
OFFLINE_TABLE_NAME,
+        ControllerGauge.SEGMENTS_WITH_LESS_REPLICAS), numSegments - 2);
+    // Only the last segment has a 1-of-4 ratio, so this is 25 only if that 
segment was examined under its own name
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
OFFLINE_TABLE_NAME,
+        ControllerGauge.PERCENT_OF_REPLICAS), 25);
+    // The sizes accumulate over exactly the segments that have metadata 
(including the graced one, whose size is
+    // counted before the grace check)
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
OFFLINE_TABLE_NAME,
+        ControllerGauge.TABLE_COMPRESSED_SIZE), expectedTableCompressedSize);
+
+    // A single batched read must cover the whole table, metadata and znode 
stats together: more than one call means the
+    // per-segment reads crept back in some form
+    ArgumentCaptor<List<String>> segmentNamesCaptor = 
ArgumentCaptor.forClass(List.class);
+    
verify(resourceManager).getSegmentsZKMetadataForSegmentNames(eq(OFFLINE_TABLE_NAME),
 segmentNamesCaptor.capture(),
+        any());
+    verify(propertyStore, never()).getStats(any(), anyInt());
+    List<String> requestedSegments = segmentNamesCaptor.getValue();
+    assertEquals(requestedSegments.size(), numSegments);
+    assertEquals(new HashSet<>(requestedSegments), 
idealState.getPartitionSet());
+  }
+
+  /// When not a single segment's ZK metadata can be read the table's gauges 
must be left alone rather than reset to
+  /// all-green values, because an all-green gauge silences the alerts that a 
stale one would still fire. Regression
+  /// test for a whole-table ZK read failure being reported as a perfectly 
healthy table.
+  @Test
+  public void tableWithoutAnyReadableSegmentZKMetadataKeepsItsGauges() {

Review Comment:
   Is this testing the opposite of `noSegmentZKMetadataTest`? Should that other 
test be updated / deleted?



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/SegmentStatusChecker.java:
##########
@@ -382,10 +383,25 @@ private boolean updateSegmentMetrics(String 
tableNameWithType, TableConfig table
     List<String> unavailableSegmentsByState = new ArrayList<>();
     List<String> unavailableSegmentsByInstance = new ArrayList<>();
 
-    for (String segment : segments) {
+    // The segment ZK metadata and the znode stats are read in a single 
batched request
+    List<String> segmentsToCheck = new ArrayList<>(segments);
+    List<Stat> segmentStats = new ArrayList<>(numSegments);
+    List<SegmentZKMetadata> segmentsZKMetadata =

Review Comment:
   Should we consider chunking this batch request instead of having an 
unbounded batch size? For tables that are super large (1M+ segments), the 
unbounded approach could cause heap spikes or even OOM on the controller, no? 
Maybe something like 5k-10k per batch will allow us to retain the speedup 
benefit but also cap the amount of heap used.



##########
pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java:
##########
@@ -639,8 +640,36 @@ public static Schema 
getTableSchema(ZkHelixPropertyStore<ZNRecord> propertyStore
     return getTableSchema(propertyStore, tableConfig.getTableName());
   }
 
+  /// Reads the ZK metadata of the named segments of the given table in a 
single batched request, and returns it
+  /// index-aligned with `segmentNames`. An entry is `null` when the segment's 
znode could not be read, either because
+  /// it does not exist or because the read failed; the two are not 
distinguished.
+  ///
+  /// When `stats` is non-null it is filled with the [Stat] of each segment's 
znode, also index-aligned with
+  /// `segmentNames`, and `null` wherever the record is `null`.
+  public static List<SegmentZKMetadata> 
getSegmentsZKMetadata(ZkHelixPropertyStore<ZNRecord> propertyStore,

Review Comment:
   `getSegmentsZKMetadata` has retries but this method does not, I'm guessing 
that wasn't intentional?



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java:
##########
@@ -1051,6 +1051,15 @@ public List<SegmentZKMetadata> 
getSegmentsZKMetadata(String tableNameWithType) {
     return ZKMetadataProvider.getSegmentsZKMetadata(_propertyStore, 
tableNameWithType);
   }
 
+  /// Reads the ZK metadata of the named segments in a single batched request, 
index-aligned with `segmentNames` and
+  /// holding `null` for segments whose znode could not be read. See
+  /// [ZKMetadataProvider#getSegmentsZKMetadata(ZkHelixPropertyStore, String, 
List, List)] for the full contract,
+  /// including the `stats` out-parameter that carries the znodes' 
modification times.
+  public List<SegmentZKMetadata> getSegmentsZKMetadataForSegmentNames(String 
tableNameWithType,

Review Comment:
   nit: this uses a new method name but the new method added in 
`ZKMetadataProvider` is an overload. Let's stick to one convention (I'm fine 
with either, so up to you).



##########
pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java:
##########
@@ -639,8 +640,36 @@ public static Schema 
getTableSchema(ZkHelixPropertyStore<ZNRecord> propertyStore
     return getTableSchema(propertyStore, tableConfig.getTableName());
   }
 
+  /// Reads the ZK metadata of the named segments of the given table in a 
single batched request, and returns it
+  /// index-aligned with `segmentNames`. An entry is `null` when the segment's 
znode could not be read, either because
+  /// it does not exist or because the read failed; the two are not 
distinguished.
+  ///
+  /// When `stats` is non-null it is filled with the [Stat] of each segment's 
znode, also index-aligned with
+  /// `segmentNames`, and `null` wherever the record is `null`.
+  public static List<SegmentZKMetadata> 
getSegmentsZKMetadata(ZkHelixPropertyStore<ZNRecord> propertyStore,
+      String tableNameWithType, List<String> segmentNames, @Nullable 
List<Stat> stats) {
+    int numSegments = segmentNames.size();
+    List<String> paths = new ArrayList<>(numSegments);
+    for (String segmentName : segmentNames) {
+      paths.add(constructPropertyStorePathForSegment(tableNameWithType, 
segmentName));
+    }
+    List<ZNRecord> znRecords = propertyStore.get(paths, stats, 
AccessOption.PERSISTENT, false);

Review Comment:
   Why are we passing `false` for the `throwException` parameter? Won't passing 
`true` instead give exactly the semantics the checker wants: deleted segment → 
`null`, failed read → exception, and `processTable` already has a catch for the 
latter?



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