This is an automated email from the ASF dual-hosted git repository.

yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 6c8f8c04b37 SegmentStatusChecker batch read the zk segment metadata 
instead of per segment (#19252)
6c8f8c04b37 is described below

commit 6c8f8c04b37fb334c30735ccd06e068c0541d733
Author: Jhow <[email protected]>
AuthorDate: Wed Aug 19 05:17:36 2026 +0800

    SegmentStatusChecker batch read the zk segment metadata instead of per 
segment (#19252)
---
 .../pinot/common/metadata/ZKMetadataProvider.java  |  31 +-
 .../controller/helix/SegmentStatusChecker.java     |  57 +++-
 .../helix/core/PinotHelixResourceManager.java      |   9 +
 .../controller/helix/SegmentStatusCheckerTest.java | 370 +++++++++++++++++----
 .../PinotHelixResourceManagerStatelessTest.java    |  46 +++
 5 files changed, 438 insertions(+), 75 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
index dc8a91a01b3..b2a9da428ba 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
@@ -20,6 +20,7 @@ package org.apache.pinot.common.metadata;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -639,8 +640,36 @@ public class ZKMetadataProvider {
     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);
+    Preconditions.checkState(znRecords.size() == numSegments,
+        "Got %s segment ZN records for %s segments of table: %s", 
znRecords.size(), numSegments, tableNameWithType);
+    Preconditions.checkState(stats == null || stats.size() == numSegments,
+        "Got %s znode stats for %s segments of table: %s", stats != null ? 
stats.size() : 0, numSegments,
+        tableNameWithType);
+    List<SegmentZKMetadata> segmentsZKMetadata = new ArrayList<>(numSegments);
+    for (ZNRecord znRecord : znRecords) {
+      segmentsZKMetadata.add(znRecord != null ? new 
SegmentZKMetadata(znRecord) : null);
+    }
+    return segmentsZKMetadata;
+  }
+
   /// NOTE: this method is very expensive, use 
[#getSegments(ZkHelixPropertyStore, String)] instead if only
-  /// segment names are needed.
+  /// segment names are needed. Segments whose ZK metadata cannot be read are 
dropped from the returned list; use
+  /// [#getSegmentsZKMetadata(ZkHelixPropertyStore, String, List, List)] when 
the result must line up with a known list
+  /// of segments, or when the znodes' [Stat] is needed.
   public static List<SegmentZKMetadata> 
getSegmentsZKMetadata(ZkHelixPropertyStore<ZNRecord> propertyStore,
       String tableNameWithType) {
     String parentPath = 
constructPropertyStorePathForResource(tableNameWithType);
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/SegmentStatusChecker.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/SegmentStatusChecker.java
index 4ed16098a86..cb27b48a9d7 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/SegmentStatusChecker.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/SegmentStatusChecker.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.controller.helix;
 
+import com.google.common.annotations.VisibleForTesting;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -78,6 +79,8 @@ public class SegmentStatusChecker extends 
ControllerPeriodicTask<SegmentStatusCh
   // log messages about disabled tables at most once a day
   private static final long DISABLED_TABLE_LOG_INTERVAL_MS = 
TimeUnit.DAYS.toMillis(1);
   private static final int MAX_SEGMENTS_TO_LOG = 10;
+  // Number of segments whose ZK metadata is read per batched request, see 
updateSegmentMetrics()
+  private static final int SEGMENT_METADATA_BATCH_SIZE = 10_000;
 
   private final int _waitForPushTimeSeconds;
   private final TableSizeReader _tableSizeReader;
@@ -88,6 +91,9 @@ public class SegmentStatusChecker extends 
ControllerPeriodicTask<SegmentStatusCh
   private final Map<String, Set<String>> _tableTenantMap = new HashMap<>();
 
   private long _lastDisabledTableLogTimestamp = 0;
+  // Overridden by the tests so that the batching can be exercised without a 
table larger than the batch size
+  @VisibleForTesting
+  int _segmentMetadataBatchSize = SEGMENT_METADATA_BATCH_SIZE;
 
   /// Constructs the segment status checker.
   /// @param pinotHelixResourceManager The resource checker used to interact 
with Helix
@@ -382,10 +388,36 @@ public class SegmentStatusChecker extends 
ControllerPeriodicTask<SegmentStatusCh
     List<String> unavailableSegmentsByState = new ArrayList<>();
     List<String> unavailableSegmentsByInstance = new ArrayList<>();
 
-    for (String segment : segments) {
+    // The segment ZK metadata and the znode stats are read with one batched 
request per SEGMENT_METADATA_BATCH_SIZE
+    // segments instead of one request per segment. The batch is bounded 
because the metadata of a whole batch is held
+    // in heap while it is being checked, which does not scale to tables with 
hundreds of thousands of segments.
+    List<String> segmentsToCheck = new ArrayList<>(segments);
+    List<SegmentZKMetadata> batchSegmentsZKMetadata = List.of();
+    List<Stat> batchSegmentStats = List.of();
+    int batchStartIndex = 0;
+    // Number of segments whose ZK metadata was read back, tracked over all 
the batches so that a table whose metadata
+    // could not be read at all is told apart from a table that is genuinely 
unhealthy
+    int numSegmentsWithZKMetadata = 0;
+
+    for (int i = 0; i < numSegments; i++) {
+      // The batches are aligned to multiples of the batch size, so a new one 
starts exactly on these indexes
+      if (i % _segmentMetadataBatchSize == 0) {
+        batchStartIndex = i;
+        int batchEndIndex = Math.min(i + _segmentMetadataBatchSize, 
numSegments);
+        batchSegmentStats = new ArrayList<>(batchEndIndex - batchStartIndex);
+        batchSegmentsZKMetadata = 
_pinotHelixResourceManager.getSegmentsZKMetadata(tableNameWithType,
+            segmentsToCheck.subList(batchStartIndex, batchEndIndex), 
batchSegmentStats);
+        for (SegmentZKMetadata segmentZKMetadata : batchSegmentsZKMetadata) {
+          if (segmentZKMetadata != null) {
+            numSegmentsWithZKMetadata++;
+          }
+        }
+      }
+      String segment = segmentsToCheck.get(i);
+      Map<String, String> isStateMap = idealState.getInstanceStateMap(segment);
       // Number of replicas in ideal state that is in ONLINE/CONSUMING state
       int numISReplicasUp = 0;
-      for (Map.Entry<String, String> entry : 
idealState.getInstanceStateMap(segment).entrySet()) {
+      for (Map.Entry<String, String> entry : isStateMap.entrySet()) {
         String state = entry.getValue();
         if (state.equals(SegmentStateModel.ONLINE) || 
state.equals(SegmentStateModel.CONSUMING)) {
           numISReplicasUp++;
@@ -397,7 +429,7 @@ public class SegmentStatusChecker extends 
ControllerPeriodicTask<SegmentStatusCh
       }
       maxISReplicasUp = Math.max(maxISReplicasUp, numISReplicasUp);
 
-      SegmentZKMetadata segmentZKMetadata = 
_pinotHelixResourceManager.getSegmentZKMetadata(tableNameWithType, segment);
+      SegmentZKMetadata segmentZKMetadata = batchSegmentsZKMetadata.get(i - 
batchStartIndex);
       // Skip the segment when it doesn't have ZK metadata. Most likely the 
segment is just deleted.
       if (segmentZKMetadata == null) {
         segmentsWithoutZKMetadata.add(segment);
@@ -419,11 +451,10 @@ public class SegmentStatusChecker extends 
ControllerPeriodicTask<SegmentStatusCh
       //       The grace window is _waitForPushTimeSeconds. Once a segment is 
older than it and still
       //       under-replicated, it is checked normally, so genuinely stuck 
commits and real replica losses still alert.
       //       The comparison uses evSnapshotTimestamp instead of 
System.currentTimeMillis() because for large tables
-      //       with many segments, the status check can take several minutes. 
A segment updated after
-      //       the EV snapshot was taken but before this individual segment 
check runs could be incorrectly flagged as
-      //       OFFLINE when using current time.
-      Stat segmentStat = propertyStore == null ? null : propertyStore.getStat(
-          
ZKMetadataProvider.constructPropertyStorePathForSegment(tableNameWithType, 
segment), AccessOption.PERSISTENT);
+      //       with many segments, the status check can still take a while. A 
segment updated after the EV snapshot was
+      //       taken but before this individual segment check runs could be 
incorrectly flagged as OFFLINE when using
+      //       current time.
+      Stat segmentStat = batchSegmentStats.get(i - batchStartIndex);
       long refTimeMs = segmentStat != null ? segmentStat.getMtime() : 
segmentZKMetadata.getCreationTime();
       if (refTimeMs > evSnapshotTimestamp - _waitForPushTimeSeconds * 1000L) {
         continue;
@@ -472,10 +503,18 @@ public class SegmentStatusChecker extends 
ControllerPeriodicTask<SegmentStatusCh
 
       minEVReplicasUp = Math.min(minEVReplicasUp, numEVReplicasUp);
       // Total number of replicas in ideal state (including ERROR/OFFLINE 
states)
-      int numISReplicasTotal = 
Math.max(idealState.getInstanceStateMap(segment).entrySet().size(), 1);
+      int numISReplicasTotal = Math.max(isStateMap.size(), 1);
       minEVReplicasUpPercent = Math.min(minEVReplicasUpPercent, 
numEVReplicasUp * 100 / numISReplicasTotal);
     }
 
+    // Not a single segment's ZK metadata could be read, so the gauges 
computed above describe nothing. Leave the
+    // table's gauges alone instead of publishing all-green values that would 
silence the alerts a stale gauge fires.
+    if (numSegmentsWithZKMetadata == 0) {
+      LOGGER.error("Failed to read the ZK metadata of all {} segments of 
table: {}, skipping the metric update",
+          numSegments, tableNameWithType);
+      return false;
+    }
+
     // Log unavailable segments in batches
     if (!unavailableSegmentsByState.isEmpty()) {
       LOGGER.warn("Table {} has {} segments marked unavailable due to 
non-ONLINE/CONSUMING states: {}",
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
index 70703680f7a..06af564d024 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
@@ -1056,6 +1056,15 @@ public class PinotHelixResourceManager {
     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> getSegmentsZKMetadata(String 
tableNameWithType,
+      List<String> segmentNames, @Nullable List<Stat> stats) {
+    return ZKMetadataProvider.getSegmentsZKMetadata(_propertyStore, 
tableNameWithType, segmentNames, stats);
+  }
+
   public Collection<String> getLastLLCCompletedSegments(String 
tableNameWithType) {
     return 
getLastLLCCompletedSegments(getSegmentsZKMetadata(tableNameWithType));
   }
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/SegmentStatusCheckerTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/SegmentStatusCheckerTest.java
index b5d390d0215..02e333eb52c 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/SegmentStatusCheckerTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/SegmentStatusCheckerTest.java
@@ -21,6 +21,8 @@ package org.apache.pinot.controller.helix;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -58,6 +60,7 @@ import org.apache.pinot.spi.utils.TimeUtils;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.apache.zookeeper.data.Stat;
+import org.mockito.ArgumentCaptor;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
@@ -68,10 +71,12 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
 
 
 @SuppressWarnings("unchecked")
@@ -125,7 +130,7 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(OFFLINE_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata segmentZKMetadata = mockPushedSegmentZKMetadata(1234, 
11111L);
-    when(resourceManager.getSegmentZKMetadata(eq(OFFLINE_TABLE_NAME), 
anyString())).thenReturn(segmentZKMetadata);
+    mockSegmentsZKMetadataForAllSegments(resourceManager, OFFLINE_TABLE_NAME, 
idealState, segmentZKMetadata);
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -142,6 +147,10 @@ public class SegmentStatusCheckerTest {
 
     runSegmentStatusChecker(resourceManager, 0);
     verifyControllerMetrics(OFFLINE_TABLE_NAME, 2, 5, 3, 2, 66, 1, 100, 2, 
2468);
+    // The metadata and the znode stats must come from batched reads: a 
per-segment read here costs two blocking
+    // ZooKeeper round trips per segment, which takes minutes on tables with 
hundreds of thousands of segments.
+    verify(resourceManager, never()).getSegmentZKMetadata(anyString(), 
anyString());
+    verify(propertyStore, never()).getStat(anyString(), anyInt());
   }
 
   private SegmentZKMetadata mockPushedSegmentZKMetadata(long sizeInBytes, long 
pushTimeMs) {
@@ -158,13 +167,10 @@ public class SegmentStatusCheckerTest {
 
   private void runSegmentStatusChecker(PinotHelixResourceManager 
resourceManager, int waitForPushTimeInSeconds,
       TableSizeReader tableSizeReader) {
-    LeadControllerManager leadControllerManager = 
mock(LeadControllerManager.class);
-    when(leadControllerManager.isLeaderForTable(anyString())).thenReturn(true);
-    ControllerConf controllerConf = mock(ControllerConf.class);
-    
when(controllerConf.getStatusCheckerWaitForPushTimeInSeconds()).thenReturn(waitForPushTimeInSeconds);
-    SegmentStatusChecker segmentStatusChecker =
-        new SegmentStatusChecker(resourceManager, leadControllerManager, 
controllerConf, _controllerMetrics,
-            tableSizeReader);
+    runSegmentStatusChecker(buildSegmentStatusChecker(resourceManager, 
waitForPushTimeInSeconds, tableSizeReader));
+  }
+
+  private void runSegmentStatusChecker(SegmentStatusChecker 
segmentStatusChecker) {
     segmentStatusChecker.start();
     segmentStatusChecker.run();
   }
@@ -297,10 +303,9 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata committedSegmentZKMetadata = 
mockCommittedSegmentZKMetadata();
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg1)).thenReturn(committedSegmentZKMetadata);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg2)).thenReturn(committedSegmentZKMetadata);
     SegmentZKMetadata consumingSegmentZKMetadata = 
mockConsumingSegmentZKMetadata(11111L);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg3)).thenReturn(consumingSegmentZKMetadata);
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME,
+        Map.of(seg1, committedSegmentZKMetadata, seg2, 
committedSegmentZKMetadata, seg3, consumingSegmentZKMetadata));
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -363,10 +368,9 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata committedSegmentZKMetadata = 
mockCommittedSegmentZKMetadata();
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg1)).thenReturn(committedSegmentZKMetadata);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg2)).thenReturn(committedSegmentZKMetadata);
     SegmentZKMetadata consumingSegmentZKMetadata = 
mockConsumingSegmentZKMetadata(11111L);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg3)).thenReturn(consumingSegmentZKMetadata);
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME,
+        Map.of(seg1, committedSegmentZKMetadata, seg2, 
committedSegmentZKMetadata, seg3, consumingSegmentZKMetadata));
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -436,10 +440,9 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata committedSegmentZKMetadata = 
mockCommittedSegmentZKMetadata();
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg1)).thenReturn(committedSegmentZKMetadata);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg2)).thenReturn(committedSegmentZKMetadata);
     SegmentZKMetadata consumingSegmentZKMetadata = 
mockConsumingSegmentZKMetadata(11111L);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg3)).thenReturn(consumingSegmentZKMetadata);
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME,
+        Map.of(seg1, committedSegmentZKMetadata, seg2, 
committedSegmentZKMetadata, seg3, consumingSegmentZKMetadata));
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -522,10 +525,9 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata committedSegmentZKMetadata = 
mockCommittedSegmentZKMetadata();
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg1)).thenReturn(committedSegmentZKMetadata);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg2)).thenReturn(committedSegmentZKMetadata);
     SegmentZKMetadata consumingSegmentZKMetadata = 
mockConsumingSegmentZKMetadata(11111L);
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg3)).thenReturn(consumingSegmentZKMetadata);
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME,
+        Map.of(seg1, committedSegmentZKMetadata, seg2, 
committedSegmentZKMetadata, seg3, consumingSegmentZKMetadata));
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -563,7 +565,7 @@ public class SegmentStatusCheckerTest {
 
   // A pauseless COMMITTING segment: done consuming but its immutable segment 
is still being built/loaded on the
   // replicas. The grace check keys off the segment znode's modification time 
(mtime), which the test drives via
-  // propertyStore.getStat(...); the metadata here only supplies status and 
size.
+  // mockSegmentsZKMetadata(...); the metadata here only supplies status and 
size.
   private SegmentZKMetadata mockCommittingSegmentZKMetadata() {
     SegmentZKMetadata segmentZKMetadata = mock(SegmentZKMetadata.class);
     when(segmentZKMetadata.getStatus()).thenReturn(Status.COMMITTING);
@@ -578,6 +580,48 @@ public class SegmentStatusCheckerTest {
     return stat;
   }
 
+  /// Stubs the single batched segment read with the metadata of 
`segmentZKMetadataMap` (segment name -> metadata) and
+  /// the znode modification times of `segmentZNodeMTimesMs` (segment name -> 
mtime in epoch millis), preserving the
+  /// index alignment with the requested segment names that 
[SegmentStatusChecker] relies on. A segment missing from
+  /// `segmentZKMetadataMap` reads back as having no ZK metadata; one missing 
from `segmentZNodeMTimesMs` reads back
+  /// with no znode stat, which makes the checker fall back to the metadata's 
creation time.
+  private void mockSegmentsZKMetadata(PinotHelixResourceManager 
resourceManager, String tableNameWithType,
+      Map<String, SegmentZKMetadata> segmentZKMetadataMap, Map<String, Long> 
segmentZNodeMTimesMs) {
+    when(resourceManager.getSegmentsZKMetadata(eq(tableNameWithType), any(), 
any())).thenAnswer(
+        invocation -> {
+          List<String> segmentNames = invocation.getArgument(1);
+          List<Stat> stats = invocation.getArgument(2);
+          List<SegmentZKMetadata> segmentsZKMetadata = new 
ArrayList<>(segmentNames.size());
+          for (String segmentName : segmentNames) {
+            segmentsZKMetadata.add(segmentZKMetadataMap.get(segmentName));
+            if (stats != null) {
+              Long mTimeMs = segmentZNodeMTimesMs.get(segmentName);
+              stats.add(mTimeMs != null ? mockStatWithMTime(mTimeMs) : null);
+            }
+          }
+          return segmentsZKMetadata;
+        });
+  }
+
+  /// Stubs the single batched segment read with metadata only, so that every 
segment reads back without a znode stat
+  /// and the checker falls back to the metadata's creation time
+  /// (see [#mockSegmentsZKMetadata(PinotHelixResourceManager, String, Map, 
Map)]).
+  private void mockSegmentsZKMetadata(PinotHelixResourceManager 
resourceManager, String tableNameWithType,
+      Map<String, SegmentZKMetadata> segmentZKMetadataMap) {
+    mockSegmentsZKMetadata(resourceManager, tableNameWithType, 
segmentZKMetadataMap, Map.of());
+  }
+
+  /// Stubs the single batched segment read so that every segment of 
`idealState` resolves to `segmentZKMetadata`
+  /// (see [#mockSegmentsZKMetadata(PinotHelixResourceManager, String, Map)]).
+  private void mockSegmentsZKMetadataForAllSegments(PinotHelixResourceManager 
resourceManager,
+      String tableNameWithType, IdealState idealState, SegmentZKMetadata 
segmentZKMetadata) {
+    Map<String, SegmentZKMetadata> segmentZKMetadataMap = new HashMap<>();
+    for (String segmentName : idealState.getPartitionSet()) {
+      segmentZKMetadataMap.put(segmentName, segmentZKMetadata);
+    }
+    mockSegmentsZKMetadata(resourceManager, tableNameWithType, 
segmentZKMetadataMap);
+  }
+
   /// A pauseless COMMITTING segment whose replicas are still building (only 
1/3 ONLINE in the external view) must not
   /// be counted as under-replicated while it is within the grace window, so 
percentOfReplicas stays at 100. Regression
   /// test for the SegmentReplicasCriticallyLowForHATable false positive on 
pauseless tables.
@@ -608,15 +652,15 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata committingSegmentZKMetadata = 
mockCommittingSegmentZKMetadata();
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg)).thenReturn(committingSegmentZKMetadata);
+    // Just committed: znode mtime is now, within the grace window.
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME, Map.of(seg, 
committingSegmentZKMetadata),
+        Map.of(seg, System.currentTimeMillis()));
 
     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);
-    // Just committed: znode mtime is now, within the grace window.
-    when(propertyStore.getStat(anyString(), 
anyInt())).thenReturn(mockStatWithMTime(System.currentTimeMillis()));
 
     // 1h grace window; the segment was just created, so it must be skipped 
and the table stays fully replicated.
     runSegmentStatusChecker(resourceManager, 3600);
@@ -626,6 +670,221 @@ public class SegmentStatusCheckerTest {
         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);
+  }
+
+  @DataProvider(name = "segmentMetadataBatchSizes")
+  public Object[][] segmentMetadataBatchSizes() {
+    // Batch size (null leaves the production default, under which the whole 
table fits in one batch) and the number of
+    // reads the 7 segments must then take. 3 does not divide 7 evenly, so the 
last batch is a partial one.
+    return new Object[][]{
+        {null, 1},
+        {3, 3}
+    };
+  }
+
+  /// The 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, whether the
+  /// table is read in one batch or in several. 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(dataProvider = "segmentMetadataBatchSizes")
+  public void segmentsStayAlignedWithTheirBatchedMetadata(Integer 
segmentMetadataBatchSize, int expectedNumBatches) {
+    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. With a batch size of 3 the
+    // segment without ZK metadata also starts a batch, so a boundary that 
shifted the pairing drops the wrong segment.
+    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
+    SegmentStatusChecker segmentStatusChecker =
+        buildSegmentStatusChecker(resourceManager, 600, 
mock(TableSizeReader.class));
+    if (segmentMetadataBatchSize != null) {
+      segmentStatusChecker._segmentMetadataBatchSize = 
segmentMetadataBatchSize;
+    }
+    runSegmentStatusChecker(segmentStatusChecker);
+
+    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);
+
+    // The reads must be batched, metadata and znode stats together: one 
request per batch and no more, otherwise the
+    // per-segment reads crept back in some form
+    ArgumentCaptor<List<String>> segmentNamesCaptor = 
ArgumentCaptor.forClass(List.class);
+    verify(resourceManager, 
times(expectedNumBatches)).getSegmentsZKMetadata(eq(OFFLINE_TABLE_NAME),
+        segmentNamesCaptor.capture(), any());
+    verify(propertyStore, never()).getStats(any(), anyInt());
+    // Every segment is requested exactly once, in batches of at most the 
batch size
+    List<String> requestedSegments = new ArrayList<>();
+    for (List<String> batch : segmentNamesCaptor.getAllValues()) {
+      assertTrue(segmentMetadataBatchSize == null || batch.size() <= 
segmentMetadataBatchSize,
+          "batch of " + batch.size() + " segments");
+      requestedSegments.addAll(batch);
+    }
+    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. This supersedes the former
+  /// `noSegmentZKMetadataTest`, which expected the all-green gauges for the 
same scenario.
+  @Test
+  public void tableWithoutAnyReadableSegmentZKMetadataKeepsItsGauges() {
+    TableConfig tableConfig =
+        new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME).setNumReplicas(2).build();
+
+    IdealState idealState = new IdealState(OFFLINE_TABLE_NAME);
+    ExternalView externalView = new ExternalView(OFFLINE_TABLE_NAME);
+    for (int i = 0; i < 3; i++) {
+      String segment = "myTable_" + i;
+      idealState.setPartitionState(segment, "pinot1", "ONLINE");
+      idealState.setPartitionState(segment, "pinot2", "ONLINE");
+      // Every segment is under-replicated, so a metric update that went ahead 
would be visibly wrong
+      externalView.setState(segment, "pinot1", "ONLINE");
+      externalView.setState(segment, "pinot2", "OFFLINE");
+    }
+    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);
+    // No segment resolves to metadata, as if every znode read had failed
+    mockSegmentsZKMetadata(resourceManager, OFFLINE_TABLE_NAME, Map.of());
+
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
+    when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
+
+    // Sentinels standing in for what a previous, successful cycle had 
published. None of them is a value this table
+    // could legitimately produce, so any of them being overwritten means the 
checker went ahead on unreadable metadata.
+    List<ControllerGauge> segmentHealthGauges =
+        List.of(ControllerGauge.PERCENT_OF_REPLICAS, 
ControllerGauge.SEGMENTS_WITH_LESS_REPLICAS,
+            ControllerGauge.PERCENT_SEGMENTS_AVAILABLE, 
ControllerGauge.SEGMENTS_IN_ERROR_STATE,
+            ControllerGauge.TABLE_COMPRESSED_SIZE);
+    for (ControllerGauge gauge : segmentHealthGauges) {
+      _controllerMetrics.setValueOfTableGauge(OFFLINE_TABLE_NAME, gauge, -1);
+    }
+
+    runSegmentStatusChecker(resourceManager, 600);
+
+    // SEGMENT_COUNT is published from the ideal state before the read, so it 
is still updated
+    assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
OFFLINE_TABLE_NAME,
+        ControllerGauge.SEGMENT_COUNT), 3);
+    for (ControllerGauge gauge : segmentHealthGauges) {
+      assertEquals(MetricValueUtils.getTableGaugeValue(_controllerMetrics, 
OFFLINE_TABLE_NAME, gauge), -1,
+          gauge.getGaugeName());
+    }
+
+    // The metrics are shared by every test in this class, so do not leave the 
sentinels behind
+    for (ControllerGauge gauge : segmentHealthGauges) {
+      _controllerMetrics.removeTableGauge(OFFLINE_TABLE_NAME, gauge);
+    }
+  }
+
   /// A COMMITTING segment that has been under-replicated for longer than the 
grace window is a genuinely stuck commit
   /// and must still be flagged (percentOfReplicas drops), so the grace does 
not mask real problems.
   @Test
@@ -654,16 +913,15 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata committingSegmentZKMetadata = 
mockCommittingSegmentZKMetadata();
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
seg)).thenReturn(committingSegmentZKMetadata);
+    // Committed 2h ago (znode mtime), still under-replicated -> a stuck 
commit, must not be graced.
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME, Map.of(seg, 
committingSegmentZKMetadata),
+        Map.of(seg, System.currentTimeMillis() - 7200000L));
 
     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);
-    // Committed 2h ago (znode mtime), still under-replicated -> a stuck 
commit, must not be graced.
-    when(propertyStore.getStat(anyString(), anyInt()))
-        .thenReturn(mockStatWithMTime(System.currentTimeMillis() - 7200000L));
 
     // 1h grace window; the segment is 2h old and still 1/3 replicas up, so it 
must be flagged (33%).
     runSegmentStatusChecker(resourceManager, 3600);
@@ -699,7 +957,7 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(OFFLINE_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata segmentZKMetadata = mockPushedSegmentZKMetadata(1234, 
11111L);
-    when(resourceManager.getSegmentZKMetadata(eq(OFFLINE_TABLE_NAME), 
anyString())).thenReturn(segmentZKMetadata);
+    mockSegmentsZKMetadataForAllSegments(resourceManager, OFFLINE_TABLE_NAME, 
idealState, segmentZKMetadata);
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -724,7 +982,7 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getAllTables()).thenReturn(List.of(OFFLINE_TABLE_NAME));
     
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
     SegmentZKMetadata segmentZKMetadata = mockPushedSegmentZKMetadata(1234, 
11111L);
-    when(resourceManager.getSegmentZKMetadata(eq(OFFLINE_TABLE_NAME), 
anyString())).thenReturn(segmentZKMetadata);
+    mockSegmentsZKMetadataForAllSegments(resourceManager, OFFLINE_TABLE_NAME, 
idealState, segmentZKMetadata);
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -790,19 +1048,15 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(OFFLINE_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata segmentZKMetadata01 = mockPushedSegmentZKMetadata(1234, 
11111L);
-    when(resourceManager.getSegmentZKMetadata(OFFLINE_TABLE_NAME, 
"myTable_0")).thenReturn(segmentZKMetadata01);
-    when(resourceManager.getSegmentZKMetadata(OFFLINE_TABLE_NAME, 
"myTable_1")).thenReturn(segmentZKMetadata01);
     SegmentZKMetadata segmentZKMetadata2 = mockPushedSegmentZKMetadata(1234, 
System.currentTimeMillis());
-    when(resourceManager.getSegmentZKMetadata(OFFLINE_TABLE_NAME, 
"myTable_2")).thenReturn(segmentZKMetadata2);
+    // myTable_2 was just pushed (znode mtime is now) so it is within the 
grace window and skipped; the others were
+    // pushed long ago.
+    mockSegmentsZKMetadata(resourceManager, OFFLINE_TABLE_NAME,
+        Map.of("myTable_0", segmentZKMetadata01, "myTable_1", 
segmentZKMetadata01, "myTable_2", segmentZKMetadata2),
+        Map.of("myTable_0", 11111L, "myTable_1", 11111L, "myTable_2", 
System.currentTimeMillis()));
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
-    // myTable_2 was just pushed (znode mtime is now) so it is within the 
grace window and skipped; the others were
-    // pushed long ago.
-    when(propertyStore.getStat(anyString(), anyInt())).thenAnswer(inv -> {
-      String path = inv.getArgument(0);
-      return mockStatWithMTime(path.endsWith("myTable_2") ? 
System.currentTimeMillis() : 11111L);
-    });
 
     runSegmentStatusChecker(resourceManager, 600);
     verifyControllerMetrics(OFFLINE_TABLE_NAME, 0, 3, 3, 2, 100, 0, 100, 0, 
3702);
@@ -820,14 +1074,14 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getAllTables()).thenReturn(List.of(REALTIME_TABLE_NAME));
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     SegmentZKMetadata updatedSegmentZKMetadata = 
mockPushedSegmentZKMetadata(1234, System.currentTimeMillis());
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
"myTable_0")).thenReturn(updatedSegmentZKMetadata);
     SegmentZKMetadata consumingSegmentZKMetadata = 
mockConsumingSegmentZKMetadata(System.currentTimeMillis());
-    when(resourceManager.getSegmentZKMetadata(REALTIME_TABLE_NAME, 
"myTable_1")).thenReturn(consumingSegmentZKMetadata);
+    // Both segments were just updated/created (znode mtime is now), so they 
are within the grace window and skipped.
+    mockSegmentsZKMetadata(resourceManager, REALTIME_TABLE_NAME,
+        Map.of("myTable_0", updatedSegmentZKMetadata, "myTable_1", 
consumingSegmentZKMetadata),
+        Map.of("myTable_0", System.currentTimeMillis(), "myTable_1", 
System.currentTimeMillis()));
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
-    // Both segments were just updated/created (znode mtime is now), so they 
are within the grace window and skipped.
-    when(propertyStore.getStat(anyString(), 
anyInt())).thenReturn(mockStatWithMTime(System.currentTimeMillis()));
 
     runSegmentStatusChecker(resourceManager, 600);
     verifyControllerMetrics(REALTIME_TABLE_NAME, 0, 2, 2, 1, 100, 0, 100, 0, 
1234);
@@ -847,7 +1101,7 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(REALTIME_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(null);
     SegmentZKMetadata segmentZKMetadata = 
mockConsumingSegmentZKMetadata(11111L);
-    when(resourceManager.getSegmentZKMetadata(eq(REALTIME_TABLE_NAME), 
anyString())).thenReturn(segmentZKMetadata);
+    mockSegmentsZKMetadataForAllSegments(resourceManager, REALTIME_TABLE_NAME, 
idealState, segmentZKMetadata);
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -856,24 +1110,6 @@ public class SegmentStatusCheckerTest {
     verifyControllerMetrics(REALTIME_TABLE_NAME, 0, 1, 1, 1, 100, 0, 100, 0, 
0);
   }
 
-  @Test
-  public void noSegmentZKMetadataTest() {
-    IdealState idealState = new IdealState(OFFLINE_TABLE_NAME);
-    idealState.setPartitionState("myTable_0", "pinot1", "ONLINE");
-    idealState.setReplicas("1");
-    idealState.setRebalanceMode(IdealState.RebalanceMode.CUSTOMIZED);
-
-    PinotHelixResourceManager resourceManager = 
mock(PinotHelixResourceManager.class);
-    
when(resourceManager.getAllTables()).thenReturn(List.of(OFFLINE_TABLE_NAME));
-    
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
-
-    ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
-    when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
-
-    runSegmentStatusChecker(resourceManager, 0);
-    verifyControllerMetrics(OFFLINE_TABLE_NAME, 0, 1, 1, 1, 100, 0, 100, 0, 0);
-  }
-
   @Test
   public void disabledTableTest()
       throws Exception {
@@ -946,7 +1182,7 @@ public class SegmentStatusCheckerTest {
     
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
     
when(resourceManager.getTableExternalView(OFFLINE_TABLE_NAME)).thenReturn(externalView);
     SegmentZKMetadata segmentZKMetadata = mockPushedSegmentZKMetadata(1234, 
11111L);
-    when(resourceManager.getSegmentZKMetadata(eq(OFFLINE_TABLE_NAME), 
anyString())).thenReturn(segmentZKMetadata);
+    mockSegmentsZKMetadataForAllSegments(resourceManager, OFFLINE_TABLE_NAME, 
idealState, segmentZKMetadata);
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -1157,7 +1393,7 @@ public class SegmentStatusCheckerTest {
     PinotHelixResourceManager resourceManager = 
mock(PinotHelixResourceManager.class);
     
when(resourceManager.getAllTables()).thenReturn(List.of(OFFLINE_TABLE_NAME));
     
when(resourceManager.getTableIdealState(OFFLINE_TABLE_NAME)).thenReturn(idealState);
-    when(resourceManager.getSegmentZKMetadata(eq(OFFLINE_TABLE_NAME), 
anyString())).thenReturn(segmentZKMetadata);
+    mockSegmentsZKMetadataForAllSegments(resourceManager, OFFLINE_TABLE_NAME, 
idealState, segmentZKMetadata);
 
     ZkHelixPropertyStore<ZNRecord> propertyStore = 
mock(ZkHelixPropertyStore.class);
     when(resourceManager.getPropertyStore()).thenReturn(propertyStore);
@@ -1336,11 +1572,15 @@ public class SegmentStatusCheckerTest {
 
   private SegmentStatusChecker 
buildSegmentStatusChecker(PinotHelixResourceManager resourceManager,
       int waitForPushTimeInSeconds) {
+    return buildSegmentStatusChecker(resourceManager, 
waitForPushTimeInSeconds, mock(TableSizeReader.class));
+  }
+
+  private SegmentStatusChecker 
buildSegmentStatusChecker(PinotHelixResourceManager resourceManager,
+      int waitForPushTimeInSeconds, TableSizeReader tableSizeReader) {
     LeadControllerManager leadControllerManager = 
mock(LeadControllerManager.class);
     when(leadControllerManager.isLeaderForTable(anyString())).thenReturn(true);
     ControllerConf controllerConf = mock(ControllerConf.class);
     
when(controllerConf.getStatusCheckerWaitForPushTimeInSeconds()).thenReturn(waitForPushTimeInSeconds);
-    TableSizeReader tableSizeReader = mock(TableSizeReader.class);
     return new SegmentStatusChecker(resourceManager, leadControllerManager, 
controllerConf, _controllerMetrics,
         tableSizeReader);
   }
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
index c04c521dd1c..64a8ad22274 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
@@ -91,6 +91,7 @@ import org.apache.pinot.spi.utils.CommonConstants.Server;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.apache.pinot.util.TestUtils;
+import org.apache.zookeeper.data.Stat;
 import org.joda.time.DateTimeZone;
 import org.joda.time.format.DateTimeFormatter;
 import org.joda.time.format.DateTimeFormatterBuilder;
@@ -588,6 +589,51 @@ public class PinotHelixResourceManagerStatelessTest 
extends ControllerTest {
     }
   }
 
+  /// Pins the contract 
[org.apache.pinot.controller.helix.SegmentStatusChecker] depends on: the 
batched read returns
+  /// the metadata and the znode [Stat] index-aligned with the requested 
segment names, with `null` in both for a
+  /// segment that has no ZK metadata.
+  @Test
+  public void testRetrieveSegmentsZKMetadataBatched() {
+    long beforeMs = System.currentTimeMillis();
+    List<String> segmentNames = List.of("testSegment0", "testSegment1", 
"testSegment2");
+    // Write ZK metadata for the first and last segment only, leaving a gap in 
the middle
+    for (String segmentName : List.of("testSegment0", "testSegment2")) {
+      SegmentZKMetadata segmentZKMetadata = new SegmentZKMetadata(segmentName);
+      segmentZKMetadata.setSizeInBytes(segmentName.hashCode());
+      ZKMetadataProvider.setSegmentZKMetadata(_propertyStore, 
OFFLINE_TABLE_NAME, segmentZKMetadata);
+    }
+
+    try {
+      List<Stat> stats = new ArrayList<>();
+      List<SegmentZKMetadata> segmentsZKMetadata =
+          _helixResourceManager.getSegmentsZKMetadata(OFFLINE_TABLE_NAME, 
segmentNames, stats);
+
+      // Both lists must line up with the requested names, so that the gap 
does not shift the entries after it
+      assertEquals(segmentsZKMetadata.size(), 3);
+      assertEquals(stats.size(), 3);
+      for (int i : new int[]{0, 2}) {
+        String segmentName = segmentNames.get(i);
+        assertNotNull(segmentsZKMetadata.get(i), segmentName);
+        assertEquals(segmentsZKMetadata.get(i).getSegmentName(), segmentName);
+        assertEquals(segmentsZKMetadata.get(i).getSizeInBytes(), 
segmentName.hashCode());
+        // A real znode mtime, not a default or the metadata's own creation 
time
+        assertNotNull(stats.get(i), segmentName);
+        assertTrue(stats.get(i).getMtime() >= beforeMs,
+            segmentName + " mtime: " + stats.get(i).getMtime() + " < " + 
beforeMs);
+      }
+      assertNull(segmentsZKMetadata.get(1));
+      assertNull(stats.get(1));
+
+      // The stats are optional
+      assertEquals(
+          _helixResourceManager.getSegmentsZKMetadata(OFFLINE_TABLE_NAME, 
segmentNames, null).size(), 3);
+    } finally {
+      for (String segmentName : List.of("testSegment0", "testSegment2")) {
+        ZKMetadataProvider.removeSegmentZKMetadata(_propertyStore, 
OFFLINE_TABLE_NAME, segmentName);
+      }
+    }
+  }
+
   @Test
   public void testUpdateSchemaDateTime() {
     String segmentName = "testSegment";


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

Reply via email to