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

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


The following commit(s) were added to refs/heads/master by this push:
     new ad106a84bcb feat: Server view improvements for historical detail 
(decommissioning, turbo segment loading, cloning) (#19253)
ad106a84bcb is described below

commit ad106a84bcbc410183a5bd3878a4ab4a50a88838
Author: Lucas Capistrant <[email protected]>
AuthorDate: Sat May 2 14:46:35 2026 -0500

    feat: Server view improvements for historical detail (decommissioning, 
turbo segment loading, cloning) (#19253)
    
    * fix bug where clone status not reset when going from N to 0 clone mappings
    
    * Make the services view show if historical is in any mode like turbo or 
decom or clone
    
    * remove outer catch all on advice of review
    
    * prettier fix
    
    * stop mocking where not necessary
    
    this was causing NPEs
    
    * Remove more unneeded mocking of clone status manager
    
    * Remove all caps, add aggregated stats, some tidying up of types and error 
reporting
    
    * better casing and nicer error msg
    
    * fix more unneeded mock locations that were very quietly causing the CI 
fails.
    
    * uncovered more issues
    
    * checkstyle fix
---
 .../server/coordinator/duty/CloneHistoricals.java  |   5 +-
 .../server/coordinator/DruidCoordinatorTest.java   |  35 +-
 .../simulate/CoordinatorSimulationBuilder.java     |   3 +-
 .../__snapshots__/services-view.spec.tsx.snap      | 668 +++++++++++----------
 .../src/views/services-view/services-view.tsx      | 315 +++++++---
 5 files changed, 601 insertions(+), 425 deletions(-)

diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/CloneHistoricals.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/CloneHistoricals.java
index da201127f60..c12193c9b0e 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/CloneHistoricals.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/CloneHistoricals.java
@@ -68,7 +68,10 @@ public class CloneHistoricals implements CoordinatorDuty
     final DruidCluster cluster = params.getDruidCluster();
 
     if (cloneServers.isEmpty()) {
-      // No servers to be cloned.
+      if (!cloneStatusManager.getStatusForAllServers().isEmpty()) {
+        // Clear the status manager if the dynamic config no longer has 
mappings to avoid showing stale clone statuses.
+        cloneStatusManager.updateStatus(Map.of());
+      }
       return params;
     }
 
diff --git 
a/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
 
b/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
index d41e91e4474..332e6ffbada 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
@@ -19,7 +19,6 @@
 
 package org.apache.druid.server.coordinator;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -34,7 +33,6 @@ import org.apache.druid.client.ServerInventoryView;
 import org.apache.druid.common.config.JacksonConfigManager;
 import org.apache.druid.curator.discovery.LatchableServiceAnnouncer;
 import org.apache.druid.discovery.DruidLeaderSelector;
-import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.java.util.common.Intervals;
 import org.apache.druid.java.util.common.concurrent.ScheduledExecutorFactory;
 import org.apache.druid.java.util.common.concurrent.ScheduledExecutors;
@@ -96,7 +94,6 @@ public class DruidCoordinatorTest
 {
   private static final long COORDINATOR_START_DELAY = 1;
   private static final long COORDINATOR_PERIOD = 100;
-  private static final ObjectMapper OBJECT_MAPPER = new DefaultObjectMapper();
 
   private DruidCoordinator coordinator;
   private SegmentsMetadataManager segmentsMetadataManager;
@@ -172,7 +169,7 @@ public class DruidCoordinatorTest
         new CompactionStatusTracker(),
         EasyMock.niceMock(CoordinatorDynamicConfigSyncer.class),
         EasyMock.niceMock(BrokerDynamicConfigSyncer.class),
-        EasyMock.niceMock(CloneStatusManager.class)
+        new CloneStatusManager()
     );
   }
 
@@ -463,7 +460,7 @@ public class DruidCoordinatorTest
   {
     EasyMock.expect(segmentsMetadataManager.isPollingDatabasePeriodically())
             .andReturn(true).anyTimes();
-    EasyMock.replay(segmentsMetadataManager);
+    EasyMock.replay(segmentsMetadataManager, metadataRuleManager);
 
     CoordinatorCustomDutyGroups emptyCustomDutyGroups = new 
CoordinatorCustomDutyGroups(ImmutableSet.of());
     coordinator = new DruidCoordinator(
@@ -485,7 +482,7 @@ public class DruidCoordinatorTest
         new CompactionStatusTracker(),
         EasyMock.niceMock(CoordinatorDynamicConfigSyncer.class),
         EasyMock.niceMock(BrokerDynamicConfigSyncer.class),
-        EasyMock.niceMock(CloneStatusManager.class)
+        new CloneStatusManager()
     );
     coordinator.start();
 
@@ -512,7 +509,7 @@ public class DruidCoordinatorTest
   {
     EasyMock.expect(segmentsMetadataManager.isPollingDatabasePeriodically())
             .andReturn(true).anyTimes();
-    EasyMock.replay(segmentsMetadataManager);
+    EasyMock.replay(segmentsMetadataManager, metadataRuleManager);
     CoordinatorCustomDutyGroup group = new CoordinatorCustomDutyGroup(
         "group1",
         Duration.standardSeconds(1),
@@ -538,7 +535,7 @@ public class DruidCoordinatorTest
         new CompactionStatusTracker(),
         EasyMock.niceMock(CoordinatorDynamicConfigSyncer.class),
         EasyMock.niceMock(BrokerDynamicConfigSyncer.class),
-        EasyMock.niceMock(CloneStatusManager.class)
+        new CloneStatusManager()
     );
     coordinator.start();
     // Since CompactSegments is not enabled in Custom Duty Group, then 
CompactSegments must be created in IndexingServiceDuties
@@ -565,7 +562,7 @@ public class DruidCoordinatorTest
   {
     EasyMock.expect(segmentsMetadataManager.isPollingDatabasePeriodically())
             .andReturn(true).anyTimes();
-    EasyMock.replay(segmentsMetadataManager);
+    EasyMock.replay(segmentsMetadataManager, metadataRuleManager);
     CoordinatorCustomDutyGroup compactSegmentCustomGroup = new 
CoordinatorCustomDutyGroup(
         "group1",
         Duration.standardSeconds(1),
@@ -591,7 +588,7 @@ public class DruidCoordinatorTest
         new CompactionStatusTracker(),
         EasyMock.niceMock(CoordinatorDynamicConfigSyncer.class),
         EasyMock.niceMock(BrokerDynamicConfigSyncer.class),
-        EasyMock.niceMock(CloneStatusManager.class)
+        new CloneStatusManager()
     );
     coordinator.start();
 
@@ -656,7 +653,7 @@ public class DruidCoordinatorTest
     
EasyMock.expect(segmentsMetadataManager.isPollingDatabasePeriodically()).andReturn(true).anyTimes();
     
EasyMock.expect(serverInventoryView.isStarted()).andReturn(true).anyTimes();
     
EasyMock.expect(serverInventoryView.getInventory()).andReturn(Collections.emptyList()).anyTimes();
-    EasyMock.replay(serverInventoryView, loadQueueTaskMaster, 
segmentsMetadataManager);
+    EasyMock.replay(serverInventoryView, loadQueueTaskMaster, 
segmentsMetadataManager, metadataRuleManager);
 
     // Create CoordinatorCustomDutyGroups
     // We will have two groups and each group has one duty
@@ -702,14 +699,18 @@ public class DruidCoordinatorTest
         new CompactionStatusTracker(),
         EasyMock.niceMock(CoordinatorDynamicConfigSyncer.class),
         EasyMock.niceMock(BrokerDynamicConfigSyncer.class),
-        EasyMock.niceMock(CloneStatusManager.class)
+        new CloneStatusManager()
     );
     coordinator.start();
-
-    // Wait until group 1 duty ran for latch1 to countdown
-    latch1.await();
-    // Wait until group 2 duty ran for latch2 to countdown
-    latch2.await();
+    try {
+      // Wait until group 1 duty ran for latch1 to countdown
+      latch1.await();
+      // Wait until group 2 duty ran for latch2 to countdown
+      latch2.await();
+    }
+    finally {
+      coordinator.stop();
+    }
   }
 
   @Test(timeout = 60_000L)
diff --git 
a/server/src/test/java/org/apache/druid/server/coordinator/simulate/CoordinatorSimulationBuilder.java
 
b/server/src/test/java/org/apache/druid/server/coordinator/simulate/CoordinatorSimulationBuilder.java
index 166728d6d7e..b6e45ad2aea 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordinator/simulate/CoordinatorSimulationBuilder.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordinator/simulate/CoordinatorSimulationBuilder.java
@@ -522,11 +522,10 @@ public class CoordinatorSimulationBuilder
 
       this.configSyncer = 
EasyMock.niceMock(CoordinatorDynamicConfigSyncer.class);
       this.brokerConfigSyncer = 
EasyMock.niceMock(BrokerDynamicConfigSyncer.class);
-      this.cloneStatusManager = EasyMock.niceMock(CloneStatusManager.class);
+      this.cloneStatusManager = new CloneStatusManager();
 
       mocks.add(configSyncer);
       mocks.add(brokerConfigSyncer);
-      mocks.add(cloneStatusManager);
     }
 
     private void setUp() throws Exception
diff --git 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
index afd053ddc75..3f265d48186 100644
--- 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
+++ 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
@@ -74,340 +74,344 @@ exports[`ServicesView renders data 1`] = `
   <Context.Provider
     value={{}}
   >
-    <ReactTable
-      AggregatedComponent={[Function]}
-      ExpanderComponent={[Function]}
-      FilterComponent={[Function]}
-      LoadingComponent={[Function]}
-      NoDataComponent={[Function]}
-      PadRowComponent={[Function]}
-      PaginationComponent={[Function]}
-      PivotValueComponent={[Function]}
-      ResizerComponent={[Function]}
-      TableComponent={[Function]}
-      TbodyComponent={[Function]}
-      TdComponent={[Function]}
-      TfootComponent={[Function]}
-      ThComponent={[Function]}
-      TheadComponent={[Function]}
-      TrComponent={[Function]}
-      TrGroupComponent={[Function]}
-      aggregatedKey="_aggregated"
-      className="centered-table -striped -highlight padded-header"
-      collapseOnDataChange={true}
-      collapseOnPageChange={true}
-      collapseOnSortingChange={true}
-      column={
-        {
-          "Aggregated": undefined,
-          "Cell": undefined,
-          "Expander": undefined,
-          "Filter": undefined,
-          "Footer": undefined,
-          "Header": undefined,
-          "Pivot": undefined,
-          "PivotValue": undefined,
-          "Placeholder": undefined,
-          "aggregate": undefined,
-          "className": "",
-          "filterAll": false,
-          "filterMethod": undefined,
-          "filterable": undefined,
-          "footerClassName": "",
-          "footerStyle": {},
-          "getFooterProps": [Function],
-          "getHeaderProps": [Function],
-          "getProps": [Function],
-          "headerClassName": "",
-          "headerStyle": {},
-          "minResizeWidth": 11,
-          "minWidth": 100,
-          "resizable": undefined,
-          "show": true,
-          "sortMethod": undefined,
-          "sortable": undefined,
-          "style": {},
-        }
-      }
-      columns={
-        [
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Service",
-            "accessor": "service",
-            "show": true,
-            "width": 300,
-          },
-          {
-            "Cell": [Function],
-            "Filter": [Function],
-            "Header": "Type",
-            "accessor": "service_type",
-            "show": true,
-            "width": 150,
-          },
-          {
-            "Cell": [Function],
-            "Header": "Tier",
-            "accessor": [Function],
-            "id": "tier",
-            "show": true,
-            "width": 180,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Host",
-            "accessor": "host",
-            "show": true,
-            "width": 200,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Port",
-            "accessor": [Function],
-            "id": "port",
-            "show": true,
-            "width": 100,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Assigned size",
-            "accessor": "curr_size",
-            "className": "padded",
-            "filterable": false,
-            "id": "curr_size",
-            "show": true,
-            "width": 100,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Effective size",
-            "accessor": "effective_size",
-            "className": "padded",
-            "filterable": false,
-            "id": "effective_size",
-            "show": true,
-            "width": 100,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Usage",
-            "accessor": [Function],
-            "className": "padded",
-            "filterable": false,
-            "id": "usage",
-            "show": true,
-            "width": 140,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Start time",
-            "accessor": "start_time",
-            "filterMethod": [Function],
-            "id": "start_time",
-            "show": true,
-            "width": 220,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Version",
-            "accessor": "version",
-            "show": true,
-            "width": 200,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": <React.Fragment>
-              Build
-              <br />
-              revision
-            </React.Fragment>,
-            "accessor": "build_revision",
-            "show": true,
-            "width": 200,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": <React.Fragment>
-              Available
-              <br />
-              processors
-            </React.Fragment>,
-            "accessor": "available_processors",
-            "className": "padded",
-            "filterable": false,
-            "show": true,
-            "width": 100,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Total memory",
-            "accessor": "total_memory",
-            "className": "padded",
-            "filterable": false,
-            "show": true,
-            "width": 120,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Labels",
-            "accessor": "labels",
-            "className": "padded",
-            "filterable": false,
-            "show": true,
-            "width": 200,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Detail",
-            "accessor": "service",
-            "className": "padded",
-            "filterable": false,
-            "id": "queue",
-            "show": true,
-            "width": 400,
-          },
-          {
-            "Aggregated": [Function],
-            "Cell": [Function],
-            "Header": "Actions",
-            "accessor": "service",
-            "filterable": false,
-            "id": "actions",
-            "show": true,
-            "sortable": false,
-            "width": 70,
-          },
-        ]
-      }
-      data={
-        [
-          [
+    <Context.Provider>
+      <Context.Provider>
+        <ReactTable
+          AggregatedComponent={[Function]}
+          ExpanderComponent={[Function]}
+          FilterComponent={[Function]}
+          LoadingComponent={[Function]}
+          NoDataComponent={[Function]}
+          PadRowComponent={[Function]}
+          PaginationComponent={[Function]}
+          PivotValueComponent={[Function]}
+          ResizerComponent={[Function]}
+          TableComponent={[Function]}
+          TbodyComponent={[Function]}
+          TdComponent={[Function]}
+          TfootComponent={[Function]}
+          ThComponent={[Function]}
+          TheadComponent={[Function]}
+          TrComponent={[Function]}
+          TrGroupComponent={[Function]}
+          aggregatedKey="_aggregated"
+          className="centered-table -striped -highlight padded-header"
+          collapseOnDataChange={true}
+          collapseOnPageChange={true}
+          collapseOnSortingChange={true}
+          column={
             {
-              "curr_size": 0,
-              "host": "localhost",
-              "is_leader": 0,
-              "max_size": 0,
-              "plaintext_port": 8082,
-              "service": "localhost:8082",
-              "service_type": "broker",
-              "start_time": 0,
-              "tier": null,
-              "tls_port": -1,
-            },
+              "Aggregated": undefined,
+              "Cell": undefined,
+              "Expander": undefined,
+              "Filter": undefined,
+              "Footer": undefined,
+              "Header": undefined,
+              "Pivot": undefined,
+              "PivotValue": undefined,
+              "Placeholder": undefined,
+              "aggregate": undefined,
+              "className": "",
+              "filterAll": false,
+              "filterMethod": undefined,
+              "filterable": undefined,
+              "footerClassName": "",
+              "footerStyle": {},
+              "getFooterProps": [Function],
+              "getHeaderProps": [Function],
+              "getProps": [Function],
+              "headerClassName": "",
+              "headerStyle": {},
+              "minResizeWidth": 11,
+              "minWidth": 100,
+              "resizable": undefined,
+              "show": true,
+              "sortMethod": undefined,
+              "sortable": undefined,
+              "style": {},
+            }
+          }
+          columns={
+            [
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Service",
+                "accessor": "service",
+                "show": true,
+                "width": 300,
+              },
+              {
+                "Cell": [Function],
+                "Filter": [Function],
+                "Header": "Type",
+                "accessor": "service_type",
+                "show": true,
+                "width": 150,
+              },
+              {
+                "Cell": [Function],
+                "Header": "Tier",
+                "accessor": [Function],
+                "id": "tier",
+                "show": true,
+                "width": 180,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Host",
+                "accessor": "host",
+                "show": true,
+                "width": 200,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Port",
+                "accessor": [Function],
+                "id": "port",
+                "show": true,
+                "width": 100,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Assigned size",
+                "accessor": "curr_size",
+                "className": "padded",
+                "filterable": false,
+                "id": "curr_size",
+                "show": true,
+                "width": 100,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Effective size",
+                "accessor": "effective_size",
+                "className": "padded",
+                "filterable": false,
+                "id": "effective_size",
+                "show": true,
+                "width": 100,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Usage",
+                "accessor": [Function],
+                "className": "padded",
+                "filterable": false,
+                "id": "usage",
+                "show": true,
+                "width": 140,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Start time",
+                "accessor": "start_time",
+                "filterMethod": [Function],
+                "id": "start_time",
+                "show": true,
+                "width": 220,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Version",
+                "accessor": "version",
+                "show": true,
+                "width": 200,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": <React.Fragment>
+                  Build
+                  <br />
+                  revision
+                </React.Fragment>,
+                "accessor": "build_revision",
+                "show": true,
+                "width": 200,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": <React.Fragment>
+                  Available
+                  <br />
+                  processors
+                </React.Fragment>,
+                "accessor": "available_processors",
+                "className": "padded",
+                "filterable": false,
+                "show": true,
+                "width": 100,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Total memory",
+                "accessor": "total_memory",
+                "className": "padded",
+                "filterable": false,
+                "show": true,
+                "width": 120,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Labels",
+                "accessor": "labels",
+                "className": "padded",
+                "filterable": false,
+                "show": true,
+                "width": 200,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Detail",
+                "accessor": "service",
+                "className": "padded wrapped",
+                "filterable": false,
+                "id": "queue",
+                "show": true,
+                "width": 400,
+              },
+              {
+                "Aggregated": [Function],
+                "Cell": [Function],
+                "Header": "Actions",
+                "accessor": "service",
+                "filterable": false,
+                "id": "actions",
+                "show": true,
+                "sortable": false,
+                "width": 70,
+              },
+            ]
+          }
+          data={
+            [
+              [
+                {
+                  "curr_size": 0,
+                  "host": "localhost",
+                  "is_leader": 0,
+                  "max_size": 0,
+                  "plaintext_port": 8082,
+                  "service": "localhost:8082",
+                  "service_type": "broker",
+                  "start_time": 0,
+                  "tier": null,
+                  "tls_port": -1,
+                },
+                {
+                  "curr_size": 179744287,
+                  "host": "localhost",
+                  "is_leader": 0,
+                  "max_size": 3000000000n,
+                  "plaintext_port": 8083,
+                  "segmentsToDrop": 0,
+                  "segmentsToDropSize": 0,
+                  "segmentsToLoad": 0,
+                  "segmentsToLoadSize": 0,
+                  "service": "localhost:8083",
+                  "service_type": "historical",
+                  "start_time": 0,
+                  "tier": "_default_tier",
+                  "tls_port": -1,
+                },
+              ],
+            ]
+          }
+          defaultExpanded={{}}
+          defaultFilterMethod={[Function]}
+          defaultFiltered={[]}
+          defaultPage={0}
+          defaultPageSize={50}
+          defaultResized={[]}
+          defaultSortDesc={false}
+          defaultSortMethod={[Function]}
+          defaultSorted={[]}
+          expanderDefaults={
             {
-              "curr_size": 179744287,
-              "host": "localhost",
-              "is_leader": 0,
-              "max_size": 3000000000n,
-              "plaintext_port": 8083,
-              "segmentsToDrop": 0,
-              "segmentsToDropSize": 0,
-              "segmentsToLoad": 0,
-              "segmentsToLoadSize": 0,
-              "service": "localhost:8083",
-              "service_type": "historical",
-              "start_time": 0,
-              "tier": "_default_tier",
-              "tls_port": -1,
-            },
-          ],
-        ]
-      }
-      defaultExpanded={{}}
-      defaultFilterMethod={[Function]}
-      defaultFiltered={[]}
-      defaultPage={0}
-      defaultPageSize={50}
-      defaultResized={[]}
-      defaultSortDesc={false}
-      defaultSortMethod={[Function]}
-      defaultSorted={[]}
-      expanderDefaults={
-        {
-          "filterable": false,
-          "resizable": false,
-          "sortable": false,
-          "width": 35,
-        }
-      }
-      filterable={true}
-      filtered={[]}
-      freezeWhenExpanded={false}
-      getLoadingProps={[Function]}
-      getNoDataProps={[Function]}
-      getPaginationProps={[Function]}
-      getProps={[Function]}
-      getResizerProps={[Function]}
-      getTableProps={[Function]}
-      getTbodyProps={[Function]}
-      getTdProps={[Function]}
-      getTfootProps={[Function]}
-      getTfootTdProps={[Function]}
-      getTfootTrProps={[Function]}
-      getTheadFilterProps={[Function]}
-      getTheadFilterThProps={[Function]}
-      getTheadFilterTrProps={[Function]}
-      getTheadGroupProps={[Function]}
-      getTheadGroupThProps={[Function]}
-      getTheadGroupTrProps={[Function]}
-      getTheadProps={[Function]}
-      getTheadThProps={[Function]}
-      getTheadTrProps={[Function]}
-      getTrGroupProps={[Function]}
-      getTrProps={[Function]}
-      groupedByPivotKey="_groupedByPivot"
-      indexKey="_index"
-      loading={false}
-      loadingText="Loading..."
-      multiSort={true}
-      nestingLevelKey="_nestingLevel"
-      nextText="Next"
-      noDataText=""
-      ofText="of"
-      onFetchData={[Function]}
-      onFilteredChange={[Function]}
-      originalKey="_original"
-      pageJumpText="jump to page"
-      pageSizeOptions={
-        [
-          50,
-          100,
-          200,
-        ]
-      }
-      pageText="Page"
-      pivotBy={[]}
-      pivotDefaults={{}}
-      pivotIDKey="_pivotID"
-      pivotValKey="_pivotVal"
-      previousText="Previous"
-      resizable={true}
-      resolveData={[Function]}
-      rowsSelectorText="rows per page"
-      rowsText="rows"
-      showPageJump={true}
-      showPageSizeOptions={true}
-      showPagination={false}
-      showPaginationBottom={true}
-      showPaginationTop={false}
-      sortable={true}
-      style={{}}
-      subRowsKey="_subRows"
-    />
+              "filterable": false,
+              "resizable": false,
+              "sortable": false,
+              "width": 35,
+            }
+          }
+          filterable={true}
+          filtered={[]}
+          freezeWhenExpanded={false}
+          getLoadingProps={[Function]}
+          getNoDataProps={[Function]}
+          getPaginationProps={[Function]}
+          getProps={[Function]}
+          getResizerProps={[Function]}
+          getTableProps={[Function]}
+          getTbodyProps={[Function]}
+          getTdProps={[Function]}
+          getTfootProps={[Function]}
+          getTfootTdProps={[Function]}
+          getTfootTrProps={[Function]}
+          getTheadFilterProps={[Function]}
+          getTheadFilterThProps={[Function]}
+          getTheadFilterTrProps={[Function]}
+          getTheadGroupProps={[Function]}
+          getTheadGroupThProps={[Function]}
+          getTheadGroupTrProps={[Function]}
+          getTheadProps={[Function]}
+          getTheadThProps={[Function]}
+          getTheadTrProps={[Function]}
+          getTrGroupProps={[Function]}
+          getTrProps={[Function]}
+          groupedByPivotKey="_groupedByPivot"
+          indexKey="_index"
+          loading={false}
+          loadingText="Loading..."
+          multiSort={true}
+          nestingLevelKey="_nestingLevel"
+          nextText="Next"
+          noDataText=""
+          ofText="of"
+          onFetchData={[Function]}
+          onFilteredChange={[Function]}
+          originalKey="_original"
+          pageJumpText="jump to page"
+          pageSizeOptions={
+            [
+              50,
+              100,
+              200,
+            ]
+          }
+          pageText="Page"
+          pivotBy={[]}
+          pivotDefaults={{}}
+          pivotIDKey="_pivotID"
+          pivotValKey="_pivotVal"
+          previousText="Previous"
+          resizable={true}
+          resolveData={[Function]}
+          rowsSelectorText="rows per page"
+          rowsText="rows"
+          showPageJump={true}
+          showPageSizeOptions={true}
+          showPagination={false}
+          showPaginationBottom={true}
+          showPaginationTop={false}
+          sortable={true}
+          style={{}}
+          subRowsKey="_subRows"
+        />
+      </Context.Provider>
+    </Context.Provider>
   </Context.Provider>
 </div>
 `;
diff --git a/web-console/src/views/services-view/services-view.tsx 
b/web-console/src/views/services-view/services-view.tsx
index 705a8b6f8f6..a6e41769f6f 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -38,7 +38,7 @@ import {
   ViewControlBar,
 } from '../../components';
 import { AsyncActionDialog, ServiceTableActionDialog } from '../../dialogs';
-import type { QueryWithContext } from '../../druid-models';
+import type { CoordinatorDynamicConfig, QueryWithContext } from 
'../../druid-models';
 import { getConsoleViewIcon } from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import {
@@ -59,6 +59,7 @@ import {
   formatDurationWithMsIfNeeded,
   formatInteger,
   getApiArray,
+  getApiArrayFromKey,
   hasOverlayOpen,
   LocalStorageBackedVisibility,
   LocalStorageKeys,
@@ -167,13 +168,36 @@ interface ServiceResultRow {
   readonly total_memory: number;
 }
 
+interface CloneStatusInfo {
+  readonly sourceServer: string;
+  readonly targetServer: string;
+  readonly state: string;
+  readonly segmentLoadsRemaining: number;
+  readonly segmentDropsRemaining: number;
+  readonly bytesToLoad: number;
+}
+
+interface ServerModeInfo {
+  readonly turboLoadingNodes: Set<string>;
+  readonly decommissioningNodes: Set<string>;
+}
+
 interface ServicesWithAuxiliaryInfo {
   readonly services: ServiceResultRow[];
   readonly loadQueueInfo: Record<string, LoadQueueInfo>;
+  readonly cloneStatus: Record<string, CloneStatusInfo>;
+  readonly serverMode: ServerModeInfo;
   readonly workerInfo: Record<string, WorkerInfo>;
 }
 
 export const LoadQueueInfoContext = createContext<Record<string, 
LoadQueueInfo>>({});
+export const CloneStatusContext = createContext<Record<string, 
CloneStatusInfo>>({});
+
+const DEFAULT_SERVER_MODE: ServerModeInfo = {
+  turboLoadingNodes: new Set(),
+  decommissioningNodes: new Set(),
+};
+export const ServerModeContext = 
createContext<ServerModeInfo>(DEFAULT_SERVER_MODE);
 
 interface LoadQueueInfo {
   readonly segmentsToDrop: NumberLike;
@@ -220,6 +244,140 @@ function aggregateLoadQueueInfos(loadQueueInfos: 
LoadQueueInfo[]): LoadQueueInfo
   };
 }
 
+interface DetailCellProps {
+  original: ServiceResultRow;
+  workerInfoLookup: Record<string, WorkerInfo>;
+}
+
+function DetailCell({ original, workerInfoLookup }: DetailCellProps) {
+  const { service_type, service, is_leader } = original;
+  const loadQueueInfoContext = useContext(LoadQueueInfoContext);
+  const cloneStatusContext = useContext(CloneStatusContext);
+  const serverModeInfo = useContext(ServerModeContext);
+
+  switch (service_type) {
+    case 'middle_manager':
+    case 'indexer': {
+      const workerInfo = workerInfoLookup[service];
+      if (!workerInfo) return null;
+
+      if (workerInfo.worker.version === '') return <>Disabled</>;
+
+      const details: string[] = [];
+      if (workerInfo.lastCompletedTaskTime) {
+        details.push(`Last completed task: 
${formatDate(workerInfo.lastCompletedTaskTime)}`);
+      }
+      if (workerInfo.blacklistedUntil) {
+        details.push(`Blacklisted until: 
${formatDate(workerInfo.blacklistedUntil)}`);
+      }
+      return <>{details.join(' ') || null}</>;
+    }
+
+    case 'coordinator':
+    case 'overlord':
+      return <>{is_leader === 1 ? 'Leader' : ''}</>;
+
+    case 'historical': {
+      const loadQueueInfo = loadQueueInfoContext[service];
+      const cloneInfo = cloneStatusContext[service];
+
+      const parts: string[] = [];
+      if (serverModeInfo.decommissioningNodes.has(service)) {
+        parts.push('Decommissioning');
+      }
+      if (serverModeInfo.turboLoadingNodes.has(service)) {
+        parts.push('Turbo Loading');
+      }
+      if (loadQueueInfo) {
+        parts.push(formatLoadQueueInfo(loadQueueInfo));
+      }
+      if (cloneInfo) {
+        if (cloneInfo.state === 'SOURCE_SERVER_MISSING') {
+          parts.push(`Clone of ${cloneInfo.sourceServer} (source missing)`);
+        } else if (cloneInfo.segmentLoadsRemaining > 0 || 
cloneInfo.segmentDropsRemaining > 0) {
+          const details: string[] = [];
+          if (cloneInfo.segmentLoadsRemaining > 0) {
+            details.push(
+              `${pluralIfNeeded(
+                cloneInfo.segmentLoadsRemaining,
+                'segment',
+              )} to load (${formatBytesCompact(cloneInfo.bytesToLoad)})`,
+            );
+          }
+          if (cloneInfo.segmentDropsRemaining > 0) {
+            details.push(`${pluralIfNeeded(cloneInfo.segmentDropsRemaining, 
'segment')} to drop`);
+          }
+          parts.push(`Cloning from ${cloneInfo.sourceServer}: 
${details.join(', ')}`);
+        } else {
+          parts.push(`Clone of ${cloneInfo.sourceServer} (synced)`);
+        }
+      }
+      return <>{parts.join('; ') || null}</>;
+    }
+
+    default:
+      return null;
+  }
+}
+
+interface AggregatedDetailCellProps {
+  subRows: { _original: ServiceResultRow }[];
+}
+
+function AggregatedDetailCell({ subRows }: AggregatedDetailCellProps) {
+  const loadQueueInfoContext = useContext(LoadQueueInfoContext);
+  const cloneStatusContext = useContext(CloneStatusContext);
+  const serverModeInfo = useContext(ServerModeContext);
+  const historicalRows = subRows.map(r => r._original).filter(r => 
r.service_type === 'historical');
+  if (!historicalRows.length) return null;
+
+  const parts: string[] = [];
+
+  const decommissioningCount = historicalRows.filter(r =>
+    serverModeInfo.decommissioningNodes.has(r.service),
+  ).length;
+  if (decommissioningCount) {
+    parts.push(`${decommissioningCount} Decommissioning`);
+  }
+
+  const turboLoadingCount = historicalRows.filter(r =>
+    serverModeInfo.turboLoadingNodes.has(r.service),
+  ).length;
+  if (turboLoadingCount) {
+    parts.push(`${turboLoadingCount} Turbo Loading`);
+  }
+
+  const loadQueueInfos: LoadQueueInfo[] = filterMap(
+    historicalRows,
+    r => loadQueueInfoContext[r.service],
+  );
+  if (loadQueueInfos.length) {
+    parts.push(formatLoadQueueInfo(aggregateLoadQueueInfos(loadQueueInfos)));
+  }
+
+  let clonedCount = 0;
+  let cloningCount = 0;
+  let cloningErrorCount = 0;
+  for (const row of historicalRows) {
+    const cloneInfo = cloneStatusContext[row.service];
+    if (!cloneInfo) continue;
+    if (cloneInfo.state === 'SOURCE_SERVER_MISSING') {
+      cloningErrorCount++;
+    } else if (cloneInfo.segmentLoadsRemaining > 0 || 
cloneInfo.segmentDropsRemaining > 0) {
+      cloningCount++;
+    } else {
+      clonedCount++;
+    }
+  }
+  const cloneParts: string[] = [];
+  if (clonedCount) cloneParts.push(`${clonedCount} cloned`);
+  if (cloningCount) cloneParts.push(`${cloningCount} cloning`);
+  if (cloningErrorCount) cloneParts.push(pluralIfNeeded(cloningErrorCount, 
'cloning error'));
+  if (cloneParts.length) parts.push(cloneParts.join(', '));
+
+  return <>{parts.join('; ') || null}</>;
+}
+
 function defaultDisplayFn(value: any): string {
   if (value === undefined || value === null) return '';
   return String(value);
@@ -366,6 +524,54 @@ ORDER BY
           });
         }
 
+        if (capabilities.hasCoordinatorAccess() && 
visibleColumns.shown('Detail')) {
+          auxiliaryQueries.push(async (servicesWithAuxiliaryInfo, signal) => {
+            const [cloneStatusResp, configResp] = await Promise.all([
+              getApiArrayFromKey<CloneStatusInfo>(
+                '/druid/coordinator/v1/config/cloneStatus',
+                'cloneStatus',
+                signal,
+              ).catch(() => {
+                AppToaster.show({
+                  icon: IconNames.ERROR,
+                  intent: Intent.DANGER,
+                  message: 'There was an error getting the clone status map',
+                });
+                return [] as CloneStatusInfo[];
+              }),
+              Api.instance
+                .get<CoordinatorDynamicConfig>('/druid/coordinator/v1/config', 
{ signal })
+                .then(r => r.data)
+                .catch(() => {
+                  AppToaster.show({
+                    icon: IconNames.ERROR,
+                    intent: Intent.DANGER,
+                    message: 'There was an error getting the coordinator 
dynamic config',
+                  });
+                  return null;
+                }),
+            ]);
+
+            const cloneStatusLookup: Record<string, CloneStatusInfo> = 
lookupBy(
+              cloneStatusResp,
+              s => s.targetServer,
+            );
+
+            return {
+              ...servicesWithAuxiliaryInfo,
+              cloneStatus: cloneStatusLookup,
+              ...(configResp
+                ? {
+                    serverMode: {
+                      turboLoadingNodes: new 
Set<string>(configResp.turboLoadingNodes || []),
+                      decommissioningNodes: new 
Set<string>(configResp.decommissioningNodes || []),
+                    },
+                  }
+                : {}),
+            };
+          });
+        }
+
         if (capabilities.hasOverlordAccess()) {
           auxiliaryQueries.push(async (servicesWithAuxiliaryInfo, signal) => {
             try {
@@ -400,7 +606,13 @@ ORDER BY
         }
 
         return new ResultWithAuxiliaryWork<ServicesWithAuxiliaryInfo>(
-          { services, loadQueueInfo: {}, workerInfo: {} },
+          {
+            services,
+            loadQueueInfo: {},
+            cloneStatus: {},
+            serverMode: DEFAULT_SERVER_MODE,
+            workerInfo: {},
+          },
           auxiliaryQueries,
         );
       },
@@ -451,30 +663,36 @@ ORDER BY
     const { filters, onFiltersChange } = this.props;
     const { servicesState, groupServicesBy, visibleColumns } = this.state;
 
-    const { services, loadQueueInfo, workerInfo } = servicesState.data || {
+    const { services, loadQueueInfo, cloneStatus, serverMode, workerInfo } = 
servicesState.data || {
       services: [],
       loadQueueInfo: {},
+      cloneStatus: {},
+      serverMode: DEFAULT_SERVER_MODE,
       workerInfo: {},
     };
 
     return (
       <LoadQueueInfoContext.Provider value={loadQueueInfo}>
-        <ReactTable
-          data={services}
-          loading={servicesState.loading}
-          noDataText={
-            servicesState.isEmpty() ? 'No services' : 
servicesState.getErrorMessage() || ''
-          }
-          filterable
-          filtered={filters.toFilters()}
-          className={`centered-table ${DEFAULT_TABLE_CLASS_NAME}`}
-          onFilteredChange={filters => 
onFiltersChange(TableFilters.fromFilters(filters))}
-          pivotBy={groupServicesBy ? [groupServicesBy] : []}
-          defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
-          pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
-          showPagination={services.length > STANDARD_TABLE_PAGE_SIZE}
-          columns={this.getTableColumns(visibleColumns, filters, 
onFiltersChange, workerInfo)}
-        />
+        <CloneStatusContext.Provider value={cloneStatus}>
+          <ServerModeContext.Provider value={serverMode}>
+            <ReactTable
+              data={services}
+              loading={servicesState.loading}
+              noDataText={
+                servicesState.isEmpty() ? 'No services' : 
servicesState.getErrorMessage() || ''
+              }
+              filterable
+              filtered={filters.toFilters()}
+              className={`centered-table ${DEFAULT_TABLE_CLASS_NAME}`}
+              onFilteredChange={filters => 
onFiltersChange(TableFilters.fromFilters(filters))}
+              pivotBy={groupServicesBy ? [groupServicesBy] : []}
+              defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
+              pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
+              showPagination={services.length > STANDARD_TABLE_PAGE_SIZE}
+              columns={this.getTableColumns(visibleColumns, filters, 
onFiltersChange, workerInfo)}
+            />
+          </ServerModeContext.Provider>
+        </CloneStatusContext.Provider>
       </LoadQueueInfoContext.Provider>
     );
   }
@@ -817,61 +1035,12 @@ ORDER BY
           id: 'queue',
           width: 400,
           filterable: false,
-          className: 'padded',
+          className: 'padded wrapped',
           accessor: 'service',
-          Cell: ({ original }) => {
-            const { service_type, service, is_leader } = original;
-            const loadQueueInfoContext = useContext(LoadQueueInfoContext);
-
-            switch (service_type) {
-              case 'middle_manager':
-              case 'indexer': {
-                const workerInfo = workerInfoLookup[service];
-                if (!workerInfo) return null;
-
-                if (workerInfo.worker.version === '') return 'Disabled';
-
-                const details: string[] = [];
-                if (workerInfo.lastCompletedTaskTime) {
-                  details.push(
-                    `Last completed task: 
${formatDate(workerInfo.lastCompletedTaskTime)}`,
-                  );
-                }
-                if (workerInfo.blacklistedUntil) {
-                  details.push(`Blacklisted until: 
${formatDate(workerInfo.blacklistedUntil)}`);
-                }
-                return details.join(' ') || null;
-              }
-
-              case 'coordinator':
-              case 'overlord':
-                return is_leader === 1 ? 'Leader' : '';
-
-              case 'historical': {
-                const loadQueueInfo = loadQueueInfoContext[service];
-                if (!loadQueueInfo) return null;
-
-                return formatLoadQueueInfo(loadQueueInfo);
-              }
-
-              default:
-                return null;
-            }
-          },
-          Aggregated: ({ subRows }) => {
-            const loadQueueInfoContext = useContext(LoadQueueInfoContext);
-            const originalRows = subRows.map(r => r._original);
-            if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
-
-            const loadQueueInfos: LoadQueueInfo[] = filterMap(
-              originalRows,
-              r => loadQueueInfoContext[r.service],
-            );
-
-            return loadQueueInfos.length
-              ? formatLoadQueueInfo(aggregateLoadQueueInfos(loadQueueInfos))
-              : '';
-          },
+          Cell: ({ original }) => (
+            <DetailCell original={original} 
workerInfoLookup={workerInfoLookup} />
+          ),
+          Aggregated: ({ subRows }) => <AggregatedDetailCell subRows={subRows} 
/>,
         },
         {
           Header: ACTION_COLUMN_LABEL,


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


Reply via email to