suvodeep-pyne commented on code in PR #19909:
URL: https://github.com/apache/pinot/pull/19909#discussion_r4339390335


##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java:
##########
@@ -2956,22 +2958,14 @@ public void repairSegmentsInErrorState(TableConfig 
tableConfig,
     boolean repairCommittingSegments =

Review Comment:
   Agreed. In a779ff97dd, `allowRepairOfCommittingSegments` is evaluated lazily 
(memoized), only when a segment needs a gated repair, so its WARN only appears 
next to the per-segment skip WARN.
   
   The rationale is now in one place, the method's Javadoc. The case comments 
only say what each case does, and the PR's private `resetErrorReplicas` helper 
and its doc are gone.



##########
pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManagerTest.java:
##########
@@ -2315,6 +2318,118 @@ public void 
testShouldRepairErrorSegmentsForPartialUpsertOrDedup() {
         
pinotLLCRealtimeSegmentManager.shouldRepairErrorSegmentsForPartialUpsertOrDedup(DisasterRecoveryMode.ALWAYS));
   }
 
+  @Test
+  public void 
testRepairSegmentsInErrorStateResetsOnlyErrorReplicasForPauselessDedupTable()
+      throws InvalidConfigException {
+    PinotHelixResourceManager mockHelixResourceManager = 
mock(PinotHelixResourceManager.class);
+    ControllerMetrics mockControllerMetrics = mock(ControllerMetrics.class);
+    FakePinotLLCRealtimeSegmentManager segmentManager =
+        new FakePinotLLCRealtimeSegmentManager(mockHelixResourceManager, 
mockControllerMetrics);
+    TableConfig tableConfig = createPauselessDedupTableConfig();
+
+    String server0 = Helix.PREFIX_OF_SERVER_INSTANCE + 0;
+    String server1 = Helix.PREFIX_OF_SERVER_INSTANCE + 1;
+    String committingPartialErrorSegment = new LLCSegmentName(RAW_TABLE_NAME, 
0, 0, CURRENT_TIME_MS).getSegmentName();
+    String committingAllErrorSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 
1, CURRENT_TIME_MS).getSegmentName();
+    String doneSegment = new LLCSegmentName(RAW_TABLE_NAME, 1, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String inProgressSegment = new LLCSegmentName(RAW_TABLE_NAME, 1, 1, 
CURRENT_TIME_MS).getSegmentName();
+
+    IdealState idealState = new IdealState(REALTIME_TABLE_NAME);
+    ExternalView externalView = new ExternalView(REALTIME_TABLE_NAME);
+    for (String segment : List.of(committingPartialErrorSegment, 
committingAllErrorSegment, doneSegment,
+        inProgressSegment)) {
+      idealState.setPartitionState(segment, server0, SegmentStateModel.ONLINE);
+      idealState.setPartitionState(segment, server1, SegmentStateModel.ONLINE);
+    }
+    externalView.setState(committingPartialErrorSegment, server0, 
SegmentStateModel.ONLINE);
+    externalView.setState(committingPartialErrorSegment, server1, 
SegmentStateModel.ERROR);
+    externalView.setState(committingAllErrorSegment, server0, 
SegmentStateModel.ERROR);
+    externalView.setState(committingAllErrorSegment, server1, 
SegmentStateModel.ERROR);
+    externalView.setState(doneSegment, server0, SegmentStateModel.ONLINE);
+    externalView.setState(doneSegment, server1, SegmentStateModel.ERROR);
+    externalView.setState(inProgressSegment, server0, SegmentStateModel.ERROR);
+    externalView.setState(inProgressSegment, server1, SegmentStateModel.ERROR);
+    segmentManager._idealState = idealState;
+    
when(mockHelixResourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
+    mockSegmentZKMetadataStatus(mockHelixResourceManager, 
committingPartialErrorSegment, Status.COMMITTING);
+    mockSegmentZKMetadataStatus(mockHelixResourceManager, 
committingAllErrorSegment, Status.COMMITTING);
+    mockSegmentZKMetadataStatus(mockHelixResourceManager, doneSegment, 
Status.DONE);
+    mockSegmentZKMetadataStatus(mockHelixResourceManager, inProgressSegment, 
Status.IN_PROGRESS);
+
+    segmentManager.repairSegmentsInErrorState(tableConfig, false);
+
+    // ERROR replicas are reset even though dedup is enabled, since a reset 
only reloads data a peer or the deep
+    // store already holds
+    verify(mockHelixResourceManager).resetSegment(REALTIME_TABLE_NAME, 
committingPartialErrorSegment, server1);
+    verify(mockHelixResourceManager).resetSegment(REALTIME_TABLE_NAME, 
doneSegment, server1);
+    verify(mockHelixResourceManager).resetSegment(REALTIME_TABLE_NAME, 
inProgressSegment, server0);
+    verify(mockHelixResourceManager).resetSegment(REALTIME_TABLE_NAME, 
inProgressSegment, server1);
+    // Healthy replicas are never reset, and no untargeted (all-replica) reset 
is issued
+    verify(mockHelixResourceManager, 
never()).resetSegment(eq(REALTIME_TABLE_NAME), anyString(), isNull());
+    verify(mockHelixResourceManager, 
never()).resetSegment(REALTIME_TABLE_NAME, committingPartialErrorSegment,
+        server0);
+    verify(mockHelixResourceManager, 
never()).resetSegment(REALTIME_TABLE_NAME, doneSegment, server0);
+    // The COMMITTING segment with all replicas in ERROR is neither reset nor 
re-ingested without the override
+    verify(mockHelixResourceManager, 
never()).resetSegment(eq(REALTIME_TABLE_NAME), eq(committingAllErrorSegment),
+        any());
+    verify(mockHelixResourceManager, 
never()).getDataInstanceAdminEndpoints(any());
+    // Every segment with an ERROR replica is counted, and only the segment 
left unrepaired is unrecoverable
+    verify(mockControllerMetrics).setOrUpdateTableGauge(REALTIME_TABLE_NAME,
+        ControllerGauge.PAUSELESS_SEGMENTS_IN_ERROR_COUNT, 4L);
+    verify(mockControllerMetrics).setOrUpdateTableGauge(REALTIME_TABLE_NAME,
+        ControllerGauge.PAUSELESS_SEGMENTS_IN_UNRECOVERABLE_ERROR_COUNT, 1L);
+  }
+
+  @Test
+  public void 
testRepairSegmentsInErrorStateReingestsCommittingSegmentForDedupTableWithOverride()
+      throws InvalidConfigException {
+    PinotHelixResourceManager mockHelixResourceManager = 
mock(PinotHelixResourceManager.class);
+    FakePinotLLCRealtimeSegmentManager segmentManager =
+        new FakePinotLLCRealtimeSegmentManager(mockHelixResourceManager);
+    TableConfig tableConfig = createPauselessDedupTableConfig();
+
+    String server0 = Helix.PREFIX_OF_SERVER_INSTANCE + 0;
+    String server1 = Helix.PREFIX_OF_SERVER_INSTANCE + 1;
+    String committingAllErrorSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 
0, CURRENT_TIME_MS).getSegmentName();
+
+    IdealState idealState = new IdealState(REALTIME_TABLE_NAME);
+    idealState.setPartitionState(committingAllErrorSegment, server0, 
SegmentStateModel.ONLINE);
+    idealState.setPartitionState(committingAllErrorSegment, server1, 
SegmentStateModel.ONLINE);
+    ExternalView externalView = new ExternalView(REALTIME_TABLE_NAME);
+    externalView.setState(committingAllErrorSegment, server0, 
SegmentStateModel.ERROR);
+    externalView.setState(committingAllErrorSegment, server1, 
SegmentStateModel.ERROR);
+    segmentManager._idealState = idealState;
+    
when(mockHelixResourceManager.getTableExternalView(REALTIME_TABLE_NAME)).thenReturn(externalView);
+    mockSegmentZKMetadataStatus(mockHelixResourceManager, 
committingAllErrorSegment, Status.COMMITTING);
+
+    segmentManager.repairSegmentsInErrorState(tableConfig, true);
+
+    // With the override, the re-ingestion path is taken (server lookup is 
attempted) and no reset is issued
+    verify(mockHelixResourceManager).getDataInstanceAdminEndpoints(any());

Review Comment:
   Right: the unstubbed `BiMap` came back null, and the NPE was swallowed. In 
a779ff97dd:
   - `triggerReingestion` is package-private (`@VisibleForTesting`), and the 
test fake records its calls.
   - The override test stubs `getDataInstanceAdminEndpoints` with a populated 
`BiMap`. It asserts that re-ingestion was triggered once, for the COMMITTING 
segment, on an endpoint of one of its servers.
   - New tests cover the missing-endpoint and failed-trigger paths.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java:
##########
@@ -3020,13 +3020,13 @@ public void repairSegmentsInErrorState(TableConfig 
tableConfig,
       if (status == Status.IN_PROGRESS && 
segmentsInErrorStateInAllReplicas.contains(segmentName)) {
         LOGGER.info("Resetting IN_PROGRESS segment: {} in table: {} since all 
replicas are in ERROR state.",
             segmentName, realtimeTableName);
-        _helixResourceManager.resetSegment(realtimeTableName, segmentName, 
null);
+        resetErrorReplicas(realtimeTableName, segmentName, currentStateMap);
         continue;
       }
 
       // 3) For all other non-IN_PROGRESS statuses with any ERROR replica, 
trigger a reset to download/refresh
       if (status != Status.IN_PROGRESS) {
-        _helixResourceManager.resetSegment(realtimeTableName, segmentName, 
null);
+        resetErrorReplicas(realtimeTableName, segmentName, currentStateMap);

Review Comment:
   Partly agreed. I first argued this state was transient, and that doesn't 
hold. For example, the other replica can be OFFLINE (its instance disabled) 
while the commit is stuck, and then every reset waits out the download timeout 
and fails again.
   
   Skipping the reset isn't safe either, though. On OFFLINE -> ONLINE the 
server first tries its local copy, and 
`tryLoadExistingSegmentWithoutRegistering` loads a local COMMITTING copy 
without a CRC check. That covers, for example, a committer that built the 
segment and then failed. The controller can't tell whether such a copy exists, 
so only the reset can find out.
   
   So in a779ff97dd the reset still runs, as on master for pauseless tables 
without dedup. A segment with no deep-store copy (COMMITTING, or committed with 
a peer download URL) and no ONLINE or CONSUMING replica is now counted in 
`PAUSELESS_SEGMENTS_IN_UNRECOVERABLE_ERROR_COUNT`, with a WARN, so a stuck one 
is visible. The wait the reset causes is bounded by the table's download 
timeout, and it ends as soon as a peer is ONLINE or the commit completes. 
Covered by `testRepairSegmentsInErrorStateCountsSegmentsWithNoOtherSource`.
   
   A per-segment attempt cap or backoff would need state that survives across 
passes and controllers. If you think it's needed, I'd rather add it for all 
pauseless tables in a separate change.



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