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

kfaraz 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 0971e423682 perf: optimize coordinator HistoricalManagement duty 
runtime (#19532)
0971e423682 is described below

commit 0971e423682fa683f500c2dd5233ce544734b553
Author: jtuglu1 <[email protected]>
AuthorDate: Thu Jul 16 06:45:07 2026 -0700

    perf: optimize coordinator HistoricalManagement duty runtime (#19532)
    
    Changes:
    - `MarkOvershadowedSegmentsAsUnused` - build the set of datasources that 
have
    overshadowed segments first, then skip timeline construction for every 
other datasource,
    both when scanning servers and when checking zero-replica segments.
    - `MarkEternityTombstonesAsUnused` - group overshadowed segments by 
datasource up front,
    then check each candidate tombstone only against its own datasource 
segments, instead of
    scanning the full overshadowed-segment list per candidate. Use cheaper 
isOvershadowed check.
    - `PrepareBalancerAndLoadQueues` - Use total counts already available in 
ImmutableDruidDataSource
    - `UpdateReplicationStatus` - Compute unavailable, under-replicated, 
deep-storage only counts in a single pass
---
 .../coordinator/ComputePlacementCostBenchmark.java | 141 ++++++++++++++++
 .../MarkOvershadowedSegmentsAsUnusedBenchmark.java | 142 ++++++++++++++++
 .../druid/client/ImmutableDruidDataSource.java     |   6 +
 .../druid/server/coordinator/DruidCluster.java     |   2 +-
 .../druid/server/coordinator/DruidCoordinator.java |  51 +++---
 .../duty/MarkEternityTombstonesAsUnused.java       |  94 +++++++----
 .../duty/MarkOvershadowedSegmentsAsUnused.java     |  32 +++-
 .../duty/PrepareBalancerAndLoadQueues.java         |  14 +-
 .../coordinator/loading/SegmentReplicaCount.java   |  10 ++
 .../loading/SegmentReplicationStatus.java          |  89 +++++++++-
 .../coordinator/loading/SegmentStatsSnapshot.java  |  78 +++++++++
 .../loading/StrategicSegmentAssigner.java          |   6 +-
 .../loading/SegmentReplicationStatusTest.java      | 187 +++++++++++++++++++++
 13 files changed, 765 insertions(+), 87 deletions(-)

diff --git 
a/benchmarks/src/test/java/org/apache/druid/server/coordinator/ComputePlacementCostBenchmark.java
 
b/benchmarks/src/test/java/org/apache/druid/server/coordinator/ComputePlacementCostBenchmark.java
new file mode 100644
index 00000000000..3ab9e0cbac2
--- /dev/null
+++ 
b/benchmarks/src/test/java/org/apache/druid/server/coordinator/ComputePlacementCostBenchmark.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.server.coordinator;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.apache.druid.client.ImmutableDruidDataSource;
+import org.apache.druid.client.ImmutableDruidServer;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.Intervals;
+import org.apache.druid.server.coordination.DruidServerMetadata;
+import org.apache.druid.server.coordination.ServerType;
+import org.apache.druid.server.coordinator.balancer.CostBalancerStrategy;
+import org.apache.druid.server.coordinator.loading.TestLoadQueuePeon;
+import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.SegmentId;
+import org.joda.time.Interval;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks {@link CostBalancerStrategy#computePlacementCost}, the 
per-server placement-cost computation the balancer
+ * invokes for every candidate server when placing a segment. The cost of a 
single call scales with the number of
+ * interval buckets the server's segments occupy, so {@code historyDays} (the 
span of daily intervals held by the
+ * server) is the primary parameter to vary.
+ */
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 2)
+@Fork(1)
+public class ComputePlacementCostBenchmark
+{
+  private static final long DAY_MILLIS = TimeUnit.DAYS.toMillis(1);
+  private static final long T0 = 
DateTimes.of("2026-01-01T00:00:00Z").getMillis();
+  private static final String DATASOURCE = "ds0";
+
+  /** Span of contiguous daily intervals held by the server. */
+  @Param({"180", "730", "3650"})
+  private int historyDays;
+
+  private ListeningExecutorService exec;
+  private ExposedCostBalancerStrategy strategy;
+  private ServerHolder server;
+  private DataSegment proposalSegment;
+
+  @Setup(Level.Trial)
+  public void setup()
+  {
+    exec = MoreExecutors.newDirectExecutorService();
+    strategy = new ExposedCostBalancerStrategy(exec);
+
+    final List<DataSegment> segments = new ArrayList<>(historyDays);
+    for (int day = 0; day < historyDays; day++) {
+      final long start = T0 - (long) (day + 1) * DAY_MILLIS;
+      segments.add(createSegment(Intervals.utc(start, start + DAY_MILLIS)));
+    }
+
+    final ImmutableDruidServer immutableServer = new ImmutableDruidServer(
+        new DruidServerMetadata("server", "host", null, 1L << 40, null, 
ServerType.HISTORICAL, "_default_tier", 0),
+        0L,
+        ImmutableMap.of(DATASOURCE, new ImmutableDruidDataSource(DATASOURCE, 
Collections.emptyMap(), segments)),
+        segments.size()
+    );
+    server = new ServerHolder(immutableServer, new TestLoadQueuePeon());
+
+    proposalSegment = createSegment(Intervals.utc(T0 - DAY_MILLIS, T0));
+  }
+
+  @TearDown(Level.Trial)
+  public void tearDown()
+  {
+    exec.shutdownNow();
+  }
+
+  @Benchmark
+  public double computePlacementCost()
+  {
+    return strategy.computePlacementCost(proposalSegment, server);
+  }
+
+  private static DataSegment createSegment(Interval interval)
+  {
+    return DataSegment.builder(SegmentId.of(DATASOURCE, interval, "v1", 0))
+                      .size(1)
+                      .build();
+  }
+
+  /**
+   * Exposes the protected {@link CostBalancerStrategy#computePlacementCost} 
so the benchmark exercises the production
+   * implementation directly.
+   */
+  private static class ExposedCostBalancerStrategy extends CostBalancerStrategy
+  {
+    ExposedCostBalancerStrategy(ListeningExecutorService exec)
+    {
+      super(exec);
+    }
+
+    @Override
+    public double computePlacementCost(DataSegment proposalSegment, 
ServerHolder server)
+    {
+      return super.computePlacementCost(proposalSegment, server);
+    }
+  }
+}
diff --git 
a/benchmarks/src/test/java/org/apache/druid/server/coordinator/MarkOvershadowedSegmentsAsUnusedBenchmark.java
 
b/benchmarks/src/test/java/org/apache/druid/server/coordinator/MarkOvershadowedSegmentsAsUnusedBenchmark.java
new file mode 100644
index 00000000000..07fab6bd3ba
--- /dev/null
+++ 
b/benchmarks/src/test/java/org/apache/druid/server/coordinator/MarkOvershadowedSegmentsAsUnusedBenchmark.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.server.coordinator;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.druid.client.DataSourcesSnapshot;
+import org.apache.druid.client.ImmutableDruidDataSource;
+import org.apache.druid.client.ImmutableDruidServer;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.server.coordination.DruidServerMetadata;
+import org.apache.druid.server.coordination.ServerType;
+import org.apache.druid.server.coordinator.balancer.RandomBalancerStrategy;
+import 
org.apache.druid.server.coordinator.duty.MarkOvershadowedSegmentsAsUnused;
+import org.apache.druid.server.coordinator.loading.SegmentLoadQueueManager;
+import org.apache.druid.server.coordinator.loading.TestLoadQueuePeon;
+import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.SegmentId;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Benchmarks the {@code MarkOvershadowedSegmentsAsUnused} coordinator duty's 
{@code run} method against a cluster where
+ * most datasources have no overshadowed segments (as in production). The duty 
builds segment timelines from the served
+ * segments only for the datasources that actually have overshadowed segments, 
so the cost is dominated by how many
+ * datasources are relevant relative to the total served.
+ */
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 2)
+@Fork(1)
+public class MarkOvershadowedSegmentsAsUnusedBenchmark
+{
+  private static final DateTime START = DateTimes.of("2024-01-01");
+
+  /** Total datasources served by the cluster. */
+  @Param({"1000"})
+  private int numDatasources;
+
+  /** Datasources that have overshadowed segments (an older version shadowed 
by a newer one). */
+  @Param({"10", "100"})
+  private int relevantDatasources;
+
+  @Param({"4"})
+  private int intervalsPerDatasource;
+
+  private MarkOvershadowedSegmentsAsUnused duty;
+  private DruidCoordinatorRuntimeParams params;
+
+  @Setup(Level.Trial)
+  public void setup()
+  {
+    final List<DataSegment> usedSegments = new ArrayList<>();
+    final ImmutableMap.Builder<String, ImmutableDruidDataSource> dataSources = 
ImmutableMap.builder();
+
+    for (int d = 0; d < numDatasources; d++) {
+      final String datasource = "ds_" + d;
+      final boolean hasOvershadowed = d < relevantDatasources;
+      final List<DataSegment> dsSegments = new ArrayList<>();
+      for (int i = 0; i < intervalsPerDatasource; i++) {
+        final Interval interval = new Interval(START.plusDays(i), 
START.plusDays(i + 1));
+        dsSegments.add(segment(datasource, interval, "v1"));
+        if (hasOvershadowed) {
+          // A newer version overshadows the v1 segment for the same interval.
+          dsSegments.add(segment(datasource, interval, "v2"));
+        }
+      }
+      usedSegments.addAll(dsSegments);
+      dataSources.put(datasource, new ImmutableDruidDataSource(datasource, 
ImmutableMap.of(), dsSegments));
+    }
+
+    final ImmutableDruidServer server = new ImmutableDruidServer(
+        new DruidServerMetadata("server", "host", null, 1L << 40, null, 
ServerType.HISTORICAL, "_default_tier", 0),
+        0L,
+        dataSources.build(),
+        usedSegments.size()
+    );
+    final DruidCluster cluster = DruidCluster
+        .builder()
+        .add(new ServerHolder(server, new TestLoadQueuePeon()))
+        .build();
+
+    params = DruidCoordinatorRuntimeParams
+        .builder()
+        
.withDataSourcesSnapshot(DataSourcesSnapshot.fromUsedSegments(usedSegments))
+        .withDruidCluster(cluster)
+        
.withDynamicConfigs(CoordinatorDynamicConfig.builder().withMarkSegmentAsUnusedDelayMillis(0).build())
+        .withBalancerStrategy(new RandomBalancerStrategy())
+        .withSegmentAssignerUsing(new SegmentLoadQueueManager(null, null))
+        .build();
+
+    // A no-op delete handler so the benchmark measures the timeline build + 
overshadow check, not the metadata write.
+    duty = new MarkOvershadowedSegmentsAsUnused((datasource, segmentIds) -> 
segmentIds.size());
+  }
+
+  @Benchmark
+  public DruidCoordinatorRuntimeParams run()
+  {
+    return duty.run(params);
+  }
+
+  private static DataSegment segment(String datasource, Interval interval, 
String version)
+  {
+    return DataSegment.builder(SegmentId.of(datasource, interval, version, 0))
+                      .size(1)
+                      .build();
+  }
+}
diff --git 
a/server/src/main/java/org/apache/druid/client/ImmutableDruidDataSource.java 
b/server/src/main/java/org/apache/druid/client/ImmutableDruidDataSource.java
index 5120681fa64..e855ca8f1fc 100644
--- a/server/src/main/java/org/apache/druid/client/ImmutableDruidDataSource.java
+++ b/server/src/main/java/org/apache/druid/client/ImmutableDruidDataSource.java
@@ -107,6 +107,12 @@ public class ImmutableDruidDataSource
     return Collections.unmodifiableCollection(idToSegments.values());
   }
 
+  @JsonIgnore
+  public int getNumSegments()
+  {
+    return idToSegments.size();
+  }
+
   @JsonIgnore
   @Nullable
   public DataSegment getSegment(SegmentId segmentId)
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCluster.java 
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCluster.java
index 5e3bcfd31de..2743cff0f4d 100644
--- a/server/src/main/java/org/apache/druid/server/coordinator/DruidCluster.java
+++ b/server/src/main/java/org/apache/druid/server/coordinator/DruidCluster.java
@@ -101,7 +101,7 @@ public class DruidCluster
     return brokers;
   }
 
-  public Iterable<String> getTierNames()
+  public Set<String> getTierNames()
   {
     return historicals.keySet();
   }
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
index 41237e35299..dd12caf904e 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
@@ -86,6 +86,7 @@ import 
org.apache.druid.server.coordinator.loading.LoadQueueTaskMaster;
 import org.apache.druid.server.coordinator.loading.SegmentLoadQueueManager;
 import org.apache.druid.server.coordinator.loading.SegmentReplicaCount;
 import org.apache.druid.server.coordinator.loading.SegmentReplicationStatus;
+import org.apache.druid.server.coordinator.loading.SegmentStatsSnapshot;
 import org.apache.druid.server.coordinator.stats.CoordinatorRunStats;
 import org.apache.druid.server.coordinator.stats.CoordinatorStat;
 import org.apache.druid.server.coordinator.stats.Dimension;
@@ -247,12 +248,10 @@ public class DruidCoordinator
 
     final Iterable<DataSegment> dataSegments = 
metadataManager.iterateAllUsedSegments();
     for (DataSegment segment : dataSegments) {
-      SegmentReplicaCount replicaCount = 
segmentReplicationStatus.getReplicaCountsInCluster(segment.getId());
-      if (replicaCount != null && (replicaCount.totalLoaded() > 0 || 
replicaCount.required() == 0)) {
-        datasourceToUnavailableSegments.addTo(segment.getDataSource(), 0);
-      } else {
-        datasourceToUnavailableSegments.addTo(segment.getDataSource(), 1);
-      }
+      datasourceToUnavailableSegments.addTo(
+          segment.getDataSource(),
+          segmentReplicationStatus.isUnavailable(segment.getId()) ? 1 : 0
+      );
     }
 
     return datasourceToUnavailableSegments;
@@ -268,8 +267,7 @@ public class DruidCoordinator
 
     final Iterable<DataSegment> dataSegments = 
metadataManager.iterateAllUsedSegments();
     for (DataSegment segment : dataSegments) {
-      SegmentReplicaCount replicaCount = 
segmentReplicationStatus.getReplicaCountsInCluster(segment.getId());
-      if (replicaCount != null && replicaCount.totalLoaded() == 0 && 
replicaCount.required() == 0) {
+      if (segmentReplicationStatus.isDeepStorageOnly(segment.getId())) {
         datasourceToDeepStorageOnlySegments.addTo(segment.getDataSource(), 1);
       }
     }
@@ -784,9 +782,11 @@ public class DruidCoordinator
     {
       broadcastSegments = params.getBroadcastSegments();
       segmentReplicationStatus = params.getSegmentReplicationStatus();
+
       if (coordinatorSegmentMetadataCache != null) {
         
coordinatorSegmentMetadataCache.updateSegmentReplicationStatus(segmentReplicationStatus);
       }
+
       return params;
     }
   }
@@ -801,25 +801,28 @@ public class DruidCoordinator
     {
       // Collect stats for unavailable and under-replicated segments
       final CoordinatorRunStats stats = params.getCoordinatorStats();
-      getDatasourceToUnavailableSegmentCount().forEach(
-          (dataSource, numUnavailable) -> stats.add(
-              Stats.Segments.UNAVAILABLE,
-              RowKey.of(Dimension.DATASOURCE, dataSource),
-              numUnavailable
-          )
+
+      // Compute unavailable, under-replicated, deep-storage only counts in a 
single pass
+      final SegmentStatsSnapshot snapshot =
+          segmentReplicationStatus == null
+          ? SegmentStatsSnapshot.empty()
+          : 
segmentReplicationStatus.computeSegmentStats(metadataManager.iterateAllUsedSegments(),
 true);
+
+      snapshot.getDatasourceToUnavailableCount().forEach(
+          (datasource, count) ->
+              stats.add(Stats.Segments.UNAVAILABLE, 
RowKey.of(Dimension.DATASOURCE, datasource), count)
       );
-      getTierToDatasourceToUnderReplicatedCount(false).forEach(
-          (tier, countsPerDatasource) -> countsPerDatasource.forEach(
-              (dataSource, underReplicatedCount) ->
-                  stats.addToSegmentStat(Stats.Segments.UNDER_REPLICATED, 
tier, dataSource, underReplicatedCount)
+
+      snapshot.getTierToDatasourceToUnderReplicatedCount().forEach(
+          (tier, datasourceToCount) -> datasourceToCount.forEach(
+              (datasource, count) ->
+                  stats.addToSegmentStat(Stats.Segments.UNDER_REPLICATED, 
tier, datasource, count)
           )
       );
-      getDatasourceToDeepStorageQueryOnlySegmentCount().forEach(
-          (dataSource, numDeepStorageOnly) -> stats.add(
-              Stats.Segments.DEEP_STORAGE_ONLY,
-              RowKey.of(Dimension.DATASOURCE, dataSource),
-              numDeepStorageOnly
-          )
+
+      snapshot.getDatasourceToDeepStorageOnlyCount().forEach(
+          (datasource, count) ->
+              stats.add(Stats.Segments.DEEP_STORAGE_ONLY, 
RowKey.of(Dimension.DATASOURCE, datasource), count)
       );
 
       return params;
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
index f332f11020c..64c70c3a826 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkEternityTombstonesAsUnused.java
@@ -19,10 +19,9 @@
 
 package org.apache.druid.server.coordinator.duty;
 
-import com.google.common.base.Optional;
 import org.apache.druid.client.DataSourcesSnapshot;
+import org.apache.druid.client.ImmutableDruidDataSource;
 import org.apache.druid.java.util.common.DateTimes;
-import org.apache.druid.java.util.common.Intervals;
 import org.apache.druid.java.util.common.granularity.Granularities;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams;
@@ -31,7 +30,6 @@ import org.apache.druid.server.coordinator.stats.Dimension;
 import org.apache.druid.server.coordinator.stats.RowKey;
 import org.apache.druid.server.coordinator.stats.Stats;
 import org.apache.druid.timeline.DataSegment;
-import org.apache.druid.timeline.Partitions;
 import org.apache.druid.timeline.SegmentId;
 import org.apache.druid.timeline.SegmentTimeline;
 import org.apache.druid.timeline.partition.TombstoneShardSpec;
@@ -82,9 +80,9 @@ public class MarkEternityTombstonesAsUnused implements 
CoordinatorDuty
   @Override
   public DruidCoordinatorRuntimeParams run(final DruidCoordinatorRuntimeParams 
params)
   {
-    DataSourcesSnapshot dataSourcesSnapshot = params.getDataSourcesSnapshot();
+    final DataSourcesSnapshot dataSourcesSnapshot = 
params.getDataSourcesSnapshot();
 
-    final Map<String, Set<SegmentId>> 
datasourceToNonOvershadowedEternityTombstones = 
+    final Map<String, Set<SegmentId>> 
datasourceToNonOvershadowedEternityTombstones =
         determineNonOvershadowedEternityTombstones(
         dataSourcesSnapshot
     );
@@ -132,43 +130,69 @@ public class MarkEternityTombstonesAsUnused implements 
CoordinatorDuty
   private Map<String, Set<SegmentId>> 
determineNonOvershadowedEternityTombstones(final DataSourcesSnapshot 
dataSourcesSnapshot)
   {
     final Map<String, Set<SegmentId>> 
datasourceToNonOvershadowedEternityTombstones = new HashMap<>();
+    final Map<String, Set<DataSegment>> overshadowedSegmentsByDatasource = new 
HashMap<>();
 
-    dataSourcesSnapshot.getDataSourcesMap().keySet().forEach((datasource) -> {
+    for (final DataSegment overshadowedSegment : 
dataSourcesSnapshot.getOvershadowedSegments()) {
+      overshadowedSegmentsByDatasource
+          .computeIfAbsent(overshadowedSegment.getDataSource(), ds -> new 
HashSet<>())
+          .add(overshadowedSegment);
+    }
+
+    for (final ImmutableDruidDataSource dataSource : 
dataSourcesSnapshot.getDataSourcesWithAllUsedSegments()) {
+      final String datasourceName = dataSource.getName();
+      // The timeline is guaranteed to be present since it is derived from the 
same set of
+      // datasources that we are iterating over.
       final SegmentTimeline usedSegmentsTimeline
-          = 
dataSourcesSnapshot.getUsedSegmentsTimelinesPerDataSource().get(datasource);
-
-      final Optional<Set<DataSegment>> usedNonOvershadowedSegments =
-          Optional.fromNullable(usedSegmentsTimeline)
-                  .transform(timeline -> 
timeline.findNonOvershadowedObjectsInInterval(
-                      Intervals.ETERNITY,
-                      Partitions.ONLY_COMPLETE
-                  ));
-
-      if (usedNonOvershadowedSegments.isPresent()) {
-        usedNonOvershadowedSegments.get().forEach(candidateSegment -> {
-          if (isNewGenerationEternityTombstone(candidateSegment)) {
-            boolean overlaps = 
dataSourcesSnapshot.getOvershadowedSegments().stream()
-                                                  .filter(overshadowedSegment 
->
-                                                              
candidateSegment.getDataSource()
-                                                                              
.equals(overshadowedSegment.getDataSource()))
-                                                  .anyMatch(
-                                                      overshadowedSegment ->
-                                                          
candidateSegment.getInterval()
-                                                                          
.overlaps(overshadowedSegment.getInterval())
-                                                  );
-            if (!overlaps) {
-              datasourceToNonOvershadowedEternityTombstones
-                  .computeIfAbsent(datasource, ds -> new HashSet<>())
-                  .add(candidateSegment.getId());
-            }
-          }
-        });
+          = 
dataSourcesSnapshot.getUsedSegmentsTimelinesPerDataSource().get(datasourceName);
+
+      for (final DataSegment candidateSegment : dataSource.getSegments()) {
+        if (shouldMarkAsUnused(candidateSegment, 
overshadowedSegmentsByDatasource, usedSegmentsTimeline)) {
+          datasourceToNonOvershadowedEternityTombstones
+              .computeIfAbsent(datasourceName, ds -> new HashSet<>())
+              .add(candidateSegment.getId());
+        }
       }
-    });
+    }
 
     return datasourceToNonOvershadowedEternityTombstones;
   }
 
+  /**
+   * A candidate segment should be marked as unused if it is a new-generation 
eternity tombstone that is neither
+   * overshadowed in the used-segments timeline nor overlapping any 
overshadowed segment in its datasource.
+   */
+  private boolean shouldMarkAsUnused(
+      final DataSegment candidateSegment,
+      final Map<String, Set<DataSegment>> overshadowedSegmentsByDatasource,
+      final SegmentTimeline usedSegmentsTimeline
+  )
+  {
+    return isNewGenerationEternityTombstone(candidateSegment)
+           && !usedSegmentsTimeline.isOvershadowed(candidateSegment)
+           && !overlapsAnyOvershadowedSegment(
+               candidateSegment,
+               
overshadowedSegmentsByDatasource.get(candidateSegment.getDataSource())
+           );
+  }
+
+  private boolean overlapsAnyOvershadowedSegment(
+      final DataSegment candidateSegment,
+      final Set<DataSegment> overshadowedSegments
+  )
+  {
+    if (overshadowedSegments == null) {
+      return false;
+    }
+
+    for (final DataSegment overshadowedSegment : overshadowedSegments) {
+      if 
(candidateSegment.getInterval().overlaps(overshadowedSegment.getInterval())) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   private boolean isNewGenerationEternityTombstone(final DataSegment segment)
   {
     return segment.isTombstone() && 
segment.getShardSpec().getNumCorePartitions() == 0 && (
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkOvershadowedSegmentsAsUnused.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkOvershadowedSegmentsAsUnused.java
index dba11d2c290..7772245ca5a 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkOvershadowedSegmentsAsUnused.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/MarkOvershadowedSegmentsAsUnused.java
@@ -39,6 +39,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Marks a segment as unused if it is overshadowed by:
@@ -80,23 +81,33 @@ public class MarkOvershadowedSegmentsAsUnused implements 
CoordinatorDuty
       return params;
     }
 
+    // Identify the datasources that actually have overshadowed segments to 
check.
+    // Timelines only need to be built for these datasources.
+    final Set<String> eligibleDatasources = allOvershadowedSegments
+        .stream()
+        .map(DataSegment::getDataSource)
+        .collect(Collectors.toSet());
+
     final DruidCluster cluster = params.getDruidCluster();
     final Map<String, SegmentTimeline> timelines = new HashMap<>();
 
     cluster.getManagedHistoricals().values().forEach(
         historicals -> historicals.forEach(
-            historical -> addSegmentsFromServer(historical, timelines)
+            historical -> addSegmentsFromServer(historical, timelines, 
eligibleDatasources)
         )
     );
     cluster.getBrokers().forEach(
-        broker -> addSegmentsFromServer(broker, timelines)
+        broker -> addSegmentsFromServer(broker, timelines, eligibleDatasources)
     );
 
     // Include all segments that require zero replicas to be loaded
     params.getSegmentAssigner().getSegmentsWithZeroRequiredReplicas().forEach(
-        (datasource, segments) -> timelines
-            .computeIfAbsent(datasource, ds -> new SegmentTimeline())
-            .addSegments(segments.iterator())
+        (datasource, segments) -> {
+          if (eligibleDatasources.contains(datasource)) {
+            timelines.computeIfAbsent(datasource, ds -> new SegmentTimeline())
+                     .addSegments(segments.iterator());
+          }
+        }
     );
 
     // Do not include segments served by ingestion services such as tasks or 
indexers,
@@ -132,12 +143,17 @@ public class MarkOvershadowedSegmentsAsUnused implements 
CoordinatorDuty
 
   private void addSegmentsFromServer(
       ServerHolder serverHolder,
-      Map<String, SegmentTimeline> timelines
+      Map<String, SegmentTimeline> timelines,
+      Set<String> eligibleDatasources
   )
   {
-    ImmutableDruidServer server = serverHolder.getServer();
+    final ImmutableDruidServer server = serverHolder.getServer();
+
+    for (final ImmutableDruidDataSource dataSource : server.getDataSources()) {
+      if (!eligibleDatasources.contains(dataSource.getName())) {
+        continue;
+      }
 
-    for (ImmutableDruidDataSource dataSource : server.getDataSources()) {
       timelines
           .computeIfAbsent(dataSource.getName(), dsName -> new 
SegmentTimeline())
           .addSegments(dataSource.getSegments().iterator());
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/PrepareBalancerAndLoadQueues.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/PrepareBalancerAndLoadQueues.java
index ec0ddcaf0d0..0ad2c1cb199 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/duty/PrepareBalancerAndLoadQueues.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/duty/PrepareBalancerAndLoadQueues.java
@@ -20,6 +20,7 @@
 package org.apache.druid.server.coordinator.duty;
 
 import org.apache.druid.client.DruidServer;
+import org.apache.druid.client.ImmutableDruidDataSource;
 import org.apache.druid.client.ImmutableDruidServer;
 import org.apache.druid.client.ServerInventoryView;
 import org.apache.druid.java.util.common.logger.Logger;
@@ -36,7 +37,6 @@ import 
org.apache.druid.server.coordinator.stats.CoordinatorRunStats;
 import org.apache.druid.server.coordinator.stats.Dimension;
 import org.apache.druid.server.coordinator.stats.RowKey;
 import org.apache.druid.server.coordinator.stats.Stats;
-import org.apache.druid.timeline.DataSegment;
 
 import java.util.HashSet;
 import java.util.List;
@@ -203,14 +203,14 @@ public class PrepareBalancerAndLoadQueues implements 
CoordinatorDuty
 
   private void collectUsedSegmentStats(DruidCoordinatorRuntimeParams params, 
CoordinatorRunStats stats)
   {
-    params.getUsedSegmentsTimelinesPerDataSource().forEach((dataSource, 
timeline) -> {
-      long totalSizeOfUsedSegments = timeline.iterateAllObjects().stream()
-                                             
.mapToLong(DataSegment::getSize).sum();
+    for (final ImmutableDruidDataSource dataSource : 
params.getDataSourcesSnapshot().getDataSourcesWithAllUsedSegments()) {
+      final long totalSizeOfUsedSegments = dataSource.getTotalSizeOfSegments();
+      final int numUsedSegments = dataSource.getNumSegments();
 
-      RowKey datasourceKey = RowKey.of(Dimension.DATASOURCE, dataSource);
+      final RowKey datasourceKey = RowKey.of(Dimension.DATASOURCE, 
dataSource.getName());
       stats.add(Stats.Segments.USED_BYTES, datasourceKey, 
totalSizeOfUsedSegments);
-      stats.add(Stats.Segments.USED, datasourceKey, timeline.getNumObjects());
-    });
+      stats.add(Stats.Segments.USED, datasourceKey, numUsedSegments);
+    }
   }
 
   private void collectDebugStats(SegmentLoadingConfig config, 
CoordinatorRunStats stats)
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicaCount.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicaCount.java
index 1f9650a59f0..8f0bf11b7d4 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicaCount.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicaCount.java
@@ -163,6 +163,16 @@ public class SegmentReplicaCount
     return Math.max(requiredAndLoadable() - totalLoaded(), 0);
   }
 
+  /**
+   * Returns a new, independent instance with the same counts as this one.
+   */
+  SegmentReplicaCount copy()
+  {
+    final SegmentReplicaCount copy = new SegmentReplicaCount();
+    copy.accumulate(this);
+    return copy;
+  }
+
   /**
    * Accumulates counts from the given {@code SegmentReplicaCount} into this 
instance.
    */
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatus.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatus.java
index 7121642f25e..ec64b00241d 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatus.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatus.java
@@ -19,12 +19,14 @@
 
 package org.apache.druid.server.coordinator.loading;
 
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2LongMap;
 import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
 import org.apache.druid.timeline.DataSegment;
 import org.apache.druid.timeline.SegmentId;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -40,15 +42,25 @@ public class SegmentReplicationStatus
 
   public SegmentReplicationStatus(Map<SegmentId, Map<String, 
SegmentReplicaCount>> replicaCountsInTier)
   {
-    this.replicaCountsInTier = ImmutableMap.copyOf(replicaCountsInTier);
-
-    final Map<SegmentId, SegmentReplicaCount> totalReplicaCounts = new 
HashMap<>();
-    replicaCountsInTier.forEach((segmentId, tierToReplicaCount) -> {
+    // Make a deep copy of replicaCountsInTier as it may be mutated further
+    // in the same coordinator cycle (e.g. by BalanceSegments)
+    final Map<SegmentId, Map<String, SegmentReplicaCount>> 
replicaCountsInTierCopy
+        = Maps.newHashMapWithExpectedSize(replicaCountsInTier.size());
+    final Map<SegmentId, SegmentReplicaCount> totalReplicaCounts
+        = Maps.newHashMapWithExpectedSize(replicaCountsInTier.size());
+    for (Map.Entry<SegmentId, Map<String, SegmentReplicaCount>> entry : 
replicaCountsInTier.entrySet()) {
+      final Map<String, SegmentReplicaCount> tierCopy = 
Maps.newHashMapWithExpectedSize(entry.getValue().size());
       final SegmentReplicaCount total = new SegmentReplicaCount();
-      tierToReplicaCount.values().forEach(total::accumulate);
-      totalReplicaCounts.put(segmentId, total);
-    });
-    this.totalReplicaCounts = ImmutableMap.copyOf(totalReplicaCounts);
+      for (Map.Entry<String, SegmentReplicaCount> tierEntry : 
entry.getValue().entrySet()) {
+        final SegmentReplicaCount countCopy = tierEntry.getValue().copy();
+        tierCopy.put(tierEntry.getKey(), countCopy);
+        total.accumulate(countCopy);
+      }
+      replicaCountsInTierCopy.put(entry.getKey(), 
Collections.unmodifiableMap(tierCopy));
+      totalReplicaCounts.put(entry.getKey(), total);
+    }
+    this.replicaCountsInTier = 
Collections.unmodifiableMap(replicaCountsInTierCopy);
+    this.totalReplicaCounts = totalReplicaCounts;
   }
 
   public SegmentReplicaCount getReplicaCountsInCluster(SegmentId segmentId)
@@ -56,6 +68,25 @@ public class SegmentReplicationStatus
     return totalReplicaCounts.get(segmentId);
   }
 
+  /**
+   * A segment is unavailable if it has no loaded replicas while still 
requiring at least one.
+   */
+  public boolean isUnavailable(SegmentId segmentId)
+  {
+    final SegmentReplicaCount totalCount = totalReplicaCounts.get(segmentId);
+    return totalCount == null || (totalCount.totalLoaded() <= 0 && 
totalCount.required() != 0);
+  }
+
+  /**
+   * A segment is deep-storage only if it is not loaded anywhere and requires 
zero replicas,
+   * so it is served exclusively from deep storage.
+   */
+  public boolean isDeepStorageOnly(SegmentId segmentId)
+  {
+    final SegmentReplicaCount totalCount = totalReplicaCounts.get(segmentId);
+    return totalCount != null && totalCount.totalLoaded() == 0 && 
totalCount.required() == 0;
+  }
+
   public Map<String, Object2LongMap<String>> 
getTierToDatasourceToUnderReplicated(
       Iterable<DataSegment> usedSegments,
       boolean ignoreMissingServers
@@ -81,4 +112,44 @@ public class SegmentReplicationStatus
 
     return tierToUnderReplicated;
   }
+
+  /**
+   * Computes unavailable, under-replicated and deep-storage-only segment 
counts in a single
+   * pass over {@code usedSegments}, instead of three independent full 
iterations. Produces
+   * results identical to calling {@link #getReplicaCountsInCluster} and
+   * {@link #getTierToDatasourceToUnderReplicated} independently for each 
segment.
+   *
+   * @param ignoreMissingServers same semantics as in {@link 
#getTierToDatasourceToUnderReplicated}.
+   */
+  public SegmentStatsSnapshot computeSegmentStats(Iterable<DataSegment> 
usedSegments, boolean ignoreMissingServers)
+  {
+    final Object2IntOpenHashMap<String> datasourceToUnavailable = new 
Object2IntOpenHashMap<>();
+    final Object2IntOpenHashMap<String> datasourceToDeepStorageOnly = new 
Object2IntOpenHashMap<>();
+    final Map<String, Object2LongMap<String>> tierToUnderReplicated = new 
HashMap<>();
+
+    for (DataSegment segment : usedSegments) {
+      final SegmentId segmentId = segment.getId();
+      final String datasource = segment.getDataSource();
+
+      // addTo with 0 ensures the datasource is present in the map even when 
it has no unavailable segments.
+      datasourceToUnavailable.addTo(datasource, isUnavailable(segmentId) ? 1 : 
0);
+      if (isDeepStorageOnly(segmentId)) {
+        datasourceToDeepStorageOnly.addTo(datasource, 1);
+      }
+
+      final Map<String, SegmentReplicaCount> tierToReplicaCount = 
replicaCountsInTier.get(segmentId);
+      if (tierToReplicaCount != null) {
+        tierToReplicaCount.forEach((tier, counts) -> {
+          final int underReplicated = ignoreMissingServers ? counts.missing() 
: counts.missingAndLoadable();
+          if (underReplicated >= 0) {
+            Object2LongOpenHashMap<String> datasourceToUnderReplicated = 
(Object2LongOpenHashMap<String>)
+                tierToUnderReplicated.computeIfAbsent(tier, ds -> new 
Object2LongOpenHashMap<>());
+            datasourceToUnderReplicated.addTo(datasource, underReplicated);
+          }
+        });
+      }
+    }
+
+    return new SegmentStatsSnapshot(datasourceToUnavailable, 
tierToUnderReplicated, datasourceToDeepStorageOnly);
+  }
 }
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentStatsSnapshot.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentStatsSnapshot.java
new file mode 100644
index 00000000000..3a7fb163fb2
--- /dev/null
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentStatsSnapshot.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.server.coordinator.loading;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMaps;
+import it.unimi.dsi.fastutil.objects.Object2LongMap;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Holder for the three segment-stat views computed together by
+ * {@link SegmentReplicationStatus#computeSegmentStats}.
+ */
+public class SegmentStatsSnapshot
+{
+  private static final SegmentStatsSnapshot EMPTY = new SegmentStatsSnapshot(
+      Object2IntMaps.emptyMap(),
+      Collections.emptyMap(),
+      Object2IntMaps.emptyMap()
+  );
+
+  private final Object2IntMap<String> datasourceToUnavailableCount;
+  private final Map<String, Object2LongMap<String>> 
tierToDatasourceToUnderReplicatedCount;
+  private final Object2IntMap<String> datasourceToDeepStorageOnlyCount;
+
+  SegmentStatsSnapshot(
+      Object2IntMap<String> datasourceToUnavailableCount,
+      Map<String, Object2LongMap<String>> 
tierToDatasourceToUnderReplicatedCount,
+      Object2IntMap<String> datasourceToDeepStorageOnlyCount
+  )
+  {
+    this.datasourceToUnavailableCount = datasourceToUnavailableCount;
+    this.tierToDatasourceToUnderReplicatedCount = 
tierToDatasourceToUnderReplicatedCount;
+    this.datasourceToDeepStorageOnlyCount = datasourceToDeepStorageOnlyCount;
+  }
+
+  /**
+   * Returns an immutable, empty snapshot, used when no replication status is 
available.
+   */
+  public static SegmentStatsSnapshot empty()
+  {
+    return EMPTY;
+  }
+
+  public Object2IntMap<String> getDatasourceToUnavailableCount()
+  {
+    return datasourceToUnavailableCount;
+  }
+
+  public Map<String, Object2LongMap<String>> 
getTierToDatasourceToUnderReplicatedCount()
+  {
+    return tierToDatasourceToUnderReplicatedCount;
+  }
+
+  public Object2IntMap<String> getDatasourceToDeepStorageOnlyCount()
+  {
+    return datasourceToDeepStorageOnlyCount;
+  }
+}
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssigner.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssigner.java
index fc6936e4df1..f65ed4c46f4 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssigner.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/StrategicSegmentAssigner.java
@@ -19,7 +19,6 @@
 
 package org.apache.druid.server.coordinator.loading;
 
-import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import org.apache.druid.client.DruidServer;
 import org.apache.druid.server.coordinator.DruidCluster;
@@ -64,6 +63,8 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
   private final RoundRobinServerSelector serverSelector;
   private final BalancerStrategy strategy;
 
+  private final Set<String> allTiersInCluster;
+
   private final boolean useRoundRobinAssignment;
   private final Map<String, Set<String>> historicalTierAliases;
   private final Map<String, String> tierToAliasName;
@@ -86,6 +87,7 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
     this.cluster = cluster;
     this.strategy = strategy;
     this.loadQueueManager = loadQueueManager;
+    this.allTiersInCluster = Set.copyOf(cluster.getTierNames());
     this.replicaCountMap = SegmentReplicaCountMap.create(cluster);
     this.replicationThrottler = createReplicationThrottler(cluster, 
loadingConfig);
     this.useRoundRobinAssignment = 
loadingConfig.isUseRoundRobinSegmentAssignment();
@@ -234,7 +236,6 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
   public void replicateSegment(DataSegment segment, Map<String, Integer> 
tierToReplicaCount)
   {
     final Map<String, Integer> effectiveTierToReplicaCount = 
expandWithAliases(tierToReplicaCount);
-    final Set<String> allTiersInCluster = 
Sets.newHashSet(cluster.getTierNames());
 
     if (effectiveTierToReplicaCount.isEmpty()) {
       // Track the counts for a segment even if it requires 0 replicas on all 
tiers
@@ -290,7 +291,6 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
   )
   {
     final Map<String, Integer> effectiveTierToReplicaCount = 
expandWithAliases(tierToReplicaCount);
-    final Set<String> allTiersInCluster = 
Sets.newHashSet(cluster.getTierNames());
 
     if (effectiveTierToReplicaCount.isEmpty()) {
       replicaCountMap.computeIfAbsent(segment.getId(), 
DruidServer.DEFAULT_TIER);
diff --git 
a/server/src/test/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatusTest.java
 
b/server/src/test/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatusTest.java
new file mode 100644
index 00000000000..5338b2f0ef9
--- /dev/null
+++ 
b/server/src/test/java/org/apache/druid/server/coordinator/loading/SegmentReplicationStatusTest.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.server.coordinator.loading;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2LongMap;
+import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.server.coordinator.CreateDataSegments;
+import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.SegmentId;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Verifies that {@link SegmentReplicationStatus#computeSegmentStats} produces 
results
+ * identical to computing unavailable, under-replicated and deep-storage-only 
counts
+ * independently via {@link 
SegmentReplicationStatus#getReplicaCountsInCluster} and
+ * {@link SegmentReplicationStatus#getTierToDatasourceToUnderReplicated}.
+ */
+public class SegmentReplicationStatusTest
+{
+  private static final String TIER_1 = "tier1";
+  private static final String TIER_2 = "tier2";
+
+  private final List<DataSegment> segments = CreateDataSegments
+      .ofDatasource("wiki")
+      .forIntervals(5, Granularities.DAY)
+      .eachOfSize(100);
+
+  @Test
+  public void testComputeSegmentStatsMatchesIndependentComputation()
+  {
+    final Map<SegmentId, Map<String, SegmentReplicaCount>> replicaCountsInTier 
= new HashMap<>();
+
+    // Segment 0: fully loaded, single tier, no under-replication.
+    replicaCountsInTier.put(segments.get(0).getId(), Map.of(TIER_1, countOf(2, 
2, 2)));
+
+    // Segment 1: unavailable (required > 0, nothing loaded).
+    replicaCountsInTier.put(segments.get(1).getId(), Map.of(TIER_1, countOf(2, 
2, 0)));
+
+    // Segment 2: deep-storage-only (required == 0, nothing loaded).
+    replicaCountsInTier.put(segments.get(2).getId(), Map.of(TIER_1, countOf(0, 
0, 0)));
+
+    // Segment 3: under-replicated in one tier, satisfied in another.
+    final Map<String, SegmentReplicaCount> tierMap3 = new HashMap<>();
+    tierMap3.put(TIER_1, countOf(2, 2, 1));
+    tierMap3.put(TIER_2, countOf(1, 1, 1));
+    replicaCountsInTier.put(segments.get(3).getId(), tierMap3);
+
+    // Segment 4: present in the used-segment list but absent from the 
replication map
+    // (e.g. metadata race) -- must be treated as unavailable, not throw.
+    // Intentionally not added to replicaCountsInTier.
+
+    final SegmentReplicationStatus status = new 
SegmentReplicationStatus(replicaCountsInTier);
+
+    for (boolean ignoreMissingServers : new boolean[]{true, false}) {
+      final SegmentStatsSnapshot snapshot =
+          status.computeSegmentStats(segments, ignoreMissingServers);
+
+      final Object2IntMap<String> expectedUnavailable = 
computeExpectedUnavailable(status, segments);
+      final Object2IntMap<String> expectedDeepStorageOnly = 
computeExpectedDeepStorageOnly(status, segments);
+      final Map<String, Object2LongMap<String>> expectedUnderReplicated =
+          status.getTierToDatasourceToUnderReplicated(segments, 
ignoreMissingServers);
+
+      Assert.assertEquals(expectedUnavailable, 
snapshot.getDatasourceToUnavailableCount());
+      Assert.assertEquals(expectedDeepStorageOnly, 
snapshot.getDatasourceToDeepStorageOnlyCount());
+      Assert.assertEquals(expectedUnderReplicated, 
snapshot.getTierToDatasourceToUnderReplicatedCount());
+    }
+  }
+
+  @Test
+  public void testIgnoreMissingServersUsesMissingNotMissingAndLoadable()
+  {
+    final Map<SegmentId, Map<String, SegmentReplicaCount>> replicaCountsInTier 
= new HashMap<>();
+
+    // required=3, requiredAndLoadable=1 (only 1 loadable server), loaded=0.
+    // missing() = 3, missingAndLoadable() = 1.
+    final SegmentReplicaCount count = new SegmentReplicaCount();
+    count.setRequired(3, 1);
+    replicaCountsInTier.put(segments.get(0).getId(), Map.of(TIER_1, count));
+
+    final SegmentReplicationStatus status = new 
SegmentReplicationStatus(replicaCountsInTier);
+    final List<DataSegment> singleSegment = List.of(segments.get(0));
+
+    final SegmentStatsSnapshot ignoreMissing =
+        status.computeSegmentStats(singleSegment, true);
+    Assert.assertEquals(
+        3L,
+        
ignoreMissing.getTierToDatasourceToUnderReplicatedCount().get(TIER_1).getLong("wiki")
+    );
+
+    final SegmentStatsSnapshot respectMissing =
+        status.computeSegmentStats(singleSegment, false);
+    Assert.assertEquals(
+        1L,
+        
respectMissing.getTierToDatasourceToUnderReplicatedCount().get(TIER_1).getLong("wiki")
+    );
+  }
+
+  @Test
+  public void testConstructorSnapshotsInputAndIsUnaffectedByLaterMutation()
+  {
+    final Map<SegmentId, Map<String, SegmentReplicaCount>> replicaCountsInTier 
= new HashMap<>();
+    final SegmentReplicaCount count = countOf(2, 2, 1);
+    final Map<String, SegmentReplicaCount> tierMap = new HashMap<>();
+    tierMap.put(TIER_1, count);
+    replicaCountsInTier.put(segments.get(0).getId(), tierMap);
+
+    final SegmentReplicationStatus status = new 
SegmentReplicationStatus(replicaCountsInTier);
+    final SegmentReplicaCount totalBefore = 
status.getReplicaCountsInCluster(segments.get(0).getId());
+    Assert.assertEquals(1, totalBefore.totalLoaded());
+
+    // Mutate the caller's inputs after construction, as BalanceSegments does 
later in the same
+    // coordinator cycle: add a brand new segment key and mutate an 
already-tracked count in place.
+    count.incrementLoaded();
+    tierMap.put(TIER_2, countOf(1, 1, 1));
+    replicaCountsInTier.put(segments.get(1).getId(), Map.of(TIER_1, countOf(1, 
1, 1)));
+
+    Assert.assertEquals(1, 
status.getReplicaCountsInCluster(segments.get(0).getId()).totalLoaded());
+    
Assert.assertNull(status.getReplicaCountsInCluster(segments.get(1).getId()));
+  }
+
+  private static SegmentReplicaCount countOf(int required, int 
requiredAndLoadable, int loaded)
+  {
+    final SegmentReplicaCount count = new SegmentReplicaCount();
+    count.setRequired(required, requiredAndLoadable);
+    for (int i = 0; i < loaded; i++) {
+      count.incrementLoaded();
+    }
+    return count;
+  }
+
+  private static Object2IntMap<String> computeExpectedUnavailable(
+      SegmentReplicationStatus status,
+      List<DataSegment> segments
+  )
+  {
+    final it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap<String> result =
+        new it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap<>();
+    for (DataSegment segment : segments) {
+      final SegmentReplicaCount rc = 
status.getReplicaCountsInCluster(segment.getId());
+      if (rc != null && (rc.totalLoaded() > 0 || rc.required() == 0)) {
+        result.addTo(segment.getDataSource(), 0);
+      } else {
+        result.addTo(segment.getDataSource(), 1);
+      }
+    }
+    return result;
+  }
+
+  private static Object2IntMap<String> computeExpectedDeepStorageOnly(
+      SegmentReplicationStatus status,
+      List<DataSegment> segments
+  )
+  {
+    final it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap<String> result =
+        new it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap<>();
+    for (DataSegment segment : segments) {
+      final SegmentReplicaCount rc = 
status.getReplicaCountsInCluster(segment.getId());
+      if (rc != null && rc.totalLoaded() == 0 && rc.required() == 0) {
+        result.addTo(segment.getDataSource(), 1);
+      }
+    }
+    return result;
+  }
+}


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

Reply via email to