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 2e61c7120a5 refactor: enforce 1 tierAlias mapping per distinct tier. 
add tierAlias dim to some metrics (#19595)
2e61c7120a5 is described below

commit 2e61c7120a562656a44473034301d4d5a584ca4e
Author: Lucas Capistrant <[email protected]>
AuthorDate: Thu Jun 18 07:26:14 2026 -0500

    refactor: enforce 1 tierAlias mapping per distinct tier. add tierAlias dim 
to some metrics (#19595)
---
 docs/configuration/index.md                        |   2 +-
 docs/operations/metrics.md                         |   8 +-
 .../coordinator/CoordinatorDynamicConfig.java      |  45 +++++++
 .../duty/PrepareBalancerAndLoadQueues.java         |  14 ++-
 .../coordinator/loading/SegmentLoadingConfig.java  |  20 ++-
 .../loading/StrategicSegmentAssigner.java          |  18 ++-
 .../druid/server/coordinator/stats/Dimension.java  |   1 +
 .../server/coordinator/rules/LoadRuleTest.java     |  42 +++++++
 .../simulate/HistoricalTierAliasTest.java          | 138 +++++++++++++++++++++
 .../server/http/CoordinatorDynamicConfigTest.java  |  46 ++++++-
 10 files changed, 321 insertions(+), 13 deletions(-)

diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index aad78964f19..f5baf4d1375 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -793,7 +793,7 @@ The following table shows the dynamic configuration 
properties for the Coordinat
 |`replicateAfterLoadTimeout`|Boolean flag for whether or not additional 
replication is needed for segments that have failed to load due to the expiry 
of `druid.coordinator.load.timeout`. If this is set to true, the Coordinator 
will attempt to replicate the failed segment on a different historical server. 
This helps improve the segment availability if there are a few slow Historicals 
in the cluster. However, the slow Historical may still load the segment later 
and the Coordinator may issu [...]
 |`turboLoadingNodes`| Experimental. List of Historical servers to place in 
turbo loading mode. These servers use a larger thread-pool to load segments 
faster but at the cost of query performance. For servers specified in 
`turboLoadingNodes`, `druid.coordinator.loadqueuepeon.http.batchSize` is 
ignored and the coordinator uses the value of the respective 
`numLoadingThreads` instead.<br/>Please use this config with caution. All 
servers should eventually be removed from this list once the se [...]
 |`cloneServers`| Experimental. Map from target Historical server to source 
Historical server which should be cloned by the target. The target Historical 
does not participate in regular segment assignment or balancing. Instead, the 
Coordinator mirrors any segment assignment made to the source Historical onto 
the target Historical, so that the target becomes an exact copy of the source. 
Segments on the target Historical do not count towards replica counts either. 
If the source disappears,  [...]
-|`historicalTierAliases`|Map from a virtual tier name to the set of real 
Historical tier names it expands to. When a load/drop rule references a virtual 
alias tier, the Coordinator replaces it with its real tiers — each receiving 
the full replica count independently. The alias key itself is never loaded to 
directly. For example, `{"hot": ["hot_1", "hot_2"]}` causes a rule of `{"hot": 
2}` to load 2 replicas on each of `hot_1` and `hot_2`; `hot` receives no direct 
assignment. An alias valu [...]
+|`historicalTierAliases`|Map from a virtual tier name to the set of real 
Historical tier names it expands to. When a load/drop rule references a virtual 
alias tier, the Coordinator replaces it with its real tiers — each receiving 
the full replica count independently. The alias key itself is never loaded to 
directly. For example, `{"hot": ["hot_1", "hot_2"]}` causes a rule of `{"hot": 
2}` to load 2 replicas on each of `hot_1` and `hot_2`; `hot` receives no direct 
assignment. An alias valu [...]
 
 ##### Smart segment loading
 
diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md
index 6f87b7f5c96..8d71c6044f8 100644
--- a/docs/operations/metrics.md
+++ b/docs/operations/metrics.md
@@ -439,10 +439,10 @@ These metrics are emitted by the Druid Coordinator in 
every run of the correspon
 |`segment/unavailable/count`|Number of unique segments left to load until all 
used segments are available for queries.|`dataSource`|0|
 |`segment/underReplicated/count`|Number of segments, including replicas, left 
to load until all used segments are available for queries.|`tier`, 
`dataSource`|0|
 |`segment/availableDeepStorageOnly/count`|Number of unique segments that are 
only available for querying directly from deep storage.|`dataSource`|Varies|
-|`tier/historical/count`|Number of available historical nodes in each 
tier.|`tier`|Varies|
-|`tier/replication/factor`|Configured maximum replication factor in each 
tier.|`tier`|Varies|
-|`tier/required/capacity`|Total capacity in bytes required in each 
tier.|`tier`|Varies|
-|`tier/total/capacity`|Total capacity in bytes available in each 
tier.|`tier`|Varies|
+|`tier/historical/count`|Number of available historical nodes in each tier. 
The `tierAlias` dimension is emitted only when the tier belongs to an alias 
configured via 
[`historicalTierAliases`](../configuration/index.md#dynamic-configuration), and 
can be used to aggregate metrics across the tiers in an alias.|`tier`, 
`tierAlias`|Varies|
+|`tier/replication/factor`|Configured maximum replication factor in each tier. 
The `tierAlias` dimension is emitted only when the tier belongs to an alias 
configured via 
[`historicalTierAliases`](../configuration/index.md#dynamic-configuration).|`tier`,
 `tierAlias`|Varies|
+|`tier/required/capacity`|Total capacity in bytes required in each tier. The 
`tierAlias` dimension is emitted only when the tier belongs to an alias 
configured via 
[`historicalTierAliases`](../configuration/index.md#dynamic-configuration).|`tier`,
 `tierAlias`|Varies|
+|`tier/total/capacity`|Total capacity in bytes available in each tier. The 
`tierAlias` dimension is emitted only when the tier belongs to an alias 
configured via 
[`historicalTierAliases`](../configuration/index.md#dynamic-configuration).|`tier`,
 `tierAlias`|Varies|
 |`compact/task/count`|Number of tasks issued in the auto compaction run.| 
|Varies|
 |`compactTask/maxSlot/count`|Maximum number of task slots available for auto 
compaction tasks in the auto compaction run.| |Varies|
 |`compactTask/availableSlot/count`|Number of currently vacant task slots out 
of the total slots allocated for auto compaction tasks. This value is computed 
as the difference between the total number of task slots allocated for auto 
compaction and the estimated number of task slots currently occupied by running 
compaction tasks. The number of sub-tasks of each compaction task is estimated 
to be `maxNumConcurrentSubTasks`.| |Varies|
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/CoordinatorDynamicConfig.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/CoordinatorDynamicConfig.java
index 66652961e36..2c83e4a2c4a 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/CoordinatorDynamicConfig.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/CoordinatorDynamicConfig.java
@@ -37,6 +37,7 @@ import javax.annotation.Nullable;
 import javax.validation.constraints.NotNull;
 import java.util.Collection;
 import java.util.EnumMap;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
@@ -84,6 +85,13 @@ public class CoordinatorDynamicConfig
    */
   private final Map<String, Set<String>> historicalTierAliases;
 
+  /**
+   * Reverse view of {@link #historicalTierAliases}: maps each physical tier 
name
+   * to the alias tier it belongs to. Derived in the constructor and used to 
tag
+   * coordinator metrics with their {@link Dimension#TIER_ALIAS}.
+   */
+  private final Map<String, String> tierToAliasName;
+
   /**
    * Stale pending segments belonging to the data sources in this list are not 
killed by {@code
    * KillStalePendingSegments}. In other words, segments in these data sources 
are "protected".
@@ -184,6 +192,7 @@ public class CoordinatorDynamicConfig
 
     this.historicalTierAliases = Configs.valueOrDefault(historicalTierAliases, 
Map.of());
     final Set<String> aliasKeys = this.historicalTierAliases.keySet();
+    final Set<String> seenTiers = new HashSet<>();
     for (Set<String> mappedTiers : this.historicalTierAliases.values()) {
       if (!Sets.intersection(mappedTiers, aliasKeys).isEmpty()) {
         throw InvalidInput.exception(
@@ -191,7 +200,32 @@ public class CoordinatorDynamicConfig
             this.historicalTierAliases
         );
       }
+      final Set<String> duplicateTiers = Sets.intersection(mappedTiers, 
seenTiers);
+      if (!duplicateTiers.isEmpty()) {
+        throw InvalidInput.exception(
+            "historicalTierAliases [%s] is invalid. Physical tier(s) %s cannot 
belong to more than one alias.",
+            this.historicalTierAliases,
+            duplicateTiers
+        );
+      }
+      seenTiers.addAll(mappedTiers);
+    }
+    this.tierToAliasName = computeTierToAliasName(this.historicalTierAliases);
+  }
+
+  /**
+   * Builds a reverse lookup from physical tier name to its alias tier name. 
Each
+   * physical tier belongs to at most one alias (enforced during validation), 
so
+   * the mapping is unambiguous.
+   */
+  private static Map<String, String> computeTierToAliasName(Map<String, 
Set<String>> aliases)
+  {
+    if (aliases.isEmpty()) {
+      return Map.of();
     }
+    final Map<String, String> reverse = new HashMap<>();
+    aliases.forEach((alias, tiers) -> tiers.forEach(tier -> reverse.put(tier, 
alias)));
+    return reverse;
   }
 
   private Map<Dimension, String> validateDebugDimensions(Map<String, String> 
debugDimensions)
@@ -364,6 +398,17 @@ public class CoordinatorDynamicConfig
     return historicalTierAliases;
   }
 
+  /**
+   * Reverse view of {@link #getHistoricalTierAliases()} mapping each physical 
tier
+   * to the alias tier it belongs to. Used to tag coordinator metrics with
+   * {@link Dimension#TIER_ALIAS}. Not serialized; derived from {@code 
historicalTierAliases}.
+   */
+  @JsonIgnore
+  public Map<String, String> getTierToAliasName()
+  {
+    return tierToAliasName;
+  }
+
   /**
    * List of servers to put in turbo-loading mode. These servers will use a 
larger thread pool to load
    * segments. This causes decreases the average time taken to load segments. 
However, this also means less resources
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 f2029023a76..ec0ddcaf0d0 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
@@ -40,6 +40,7 @@ import org.apache.druid.timeline.DataSegment;
 
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
@@ -92,7 +93,7 @@ public class PrepareBalancerAndLoadQueues implements 
CoordinatorDuty
     cancelLoadsOnDecommissioningServers(cluster);
 
     final CoordinatorRunStats stats = params.getCoordinatorStats();
-    collectHistoricalStats(cluster, stats);
+    collectHistoricalStats(cluster, stats, dynamicConfig.getTierToAliasName());
     collectUsedSegmentStats(params, stats);
     collectDebugStats(segmentLoadingConfig, stats);
 
@@ -170,10 +171,17 @@ public class PrepareBalancerAndLoadQueues implements 
CoordinatorDuty
     return cluster.build();
   }
 
-  private void collectHistoricalStats(DruidCluster cluster, 
CoordinatorRunStats stats)
+  private void collectHistoricalStats(
+      DruidCluster cluster,
+      CoordinatorRunStats stats,
+      Map<String, String> tierToAliasName
+  )
   {
     cluster.getHistoricals().forEach((tier, historicals) -> {
-      RowKey rowKey = RowKey.of(Dimension.TIER, tier);
+      final String alias = tierToAliasName.get(tier);
+      final RowKey rowKey = alias == null
+                            ? RowKey.of(Dimension.TIER, tier)
+                            : RowKey.with(Dimension.TIER, 
tier).and(Dimension.TIER_ALIAS, alias);
       stats.add(Stats.Tier.HISTORICAL_COUNT, rowKey, historicals.size());
 
       long totalCapacity = 0;
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentLoadingConfig.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentLoadingConfig.java
index d89c009f472..44f842ddcac 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentLoadingConfig.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/loading/SegmentLoadingConfig.java
@@ -42,6 +42,7 @@ public class SegmentLoadingConfig
   private final boolean useRoundRobinSegmentAssignment;
 
   private final Map<String, Set<String>> historicalTierAliases;
+  private final Map<String, String> tierToAliasName;
 
   /**
    * Creates a new SegmentLoadingConfig with recomputed coordinator config 
values
@@ -67,7 +68,8 @@ public class SegmentLoadingConfig
           60,
           true,
           numBalancerThreads,
-          dynamicConfig.getHistoricalTierAliases()
+          dynamicConfig.getHistoricalTierAliases(),
+          dynamicConfig.getTierToAliasName()
       );
     } else {
       // Use the configured values
@@ -77,7 +79,8 @@ public class SegmentLoadingConfig
           dynamicConfig.getReplicantLifetime(),
           dynamicConfig.isUseRoundRobinSegmentAssignment(),
           dynamicConfig.getBalancerComputeThreads(),
-          dynamicConfig.getHistoricalTierAliases()
+          dynamicConfig.getHistoricalTierAliases(),
+          dynamicConfig.getTierToAliasName()
       );
     }
   }
@@ -88,7 +91,8 @@ public class SegmentLoadingConfig
       int maxLifetimeInLoadQueue,
       boolean useRoundRobinSegmentAssignment,
       int balancerComputeThreads,
-      Map<String, Set<String>> historicalTierAliases
+      Map<String, Set<String>> historicalTierAliases,
+      Map<String, String> tierToAliasName
   )
   {
     this.maxSegmentsInLoadQueue = maxSegmentsInLoadQueue;
@@ -97,6 +101,7 @@ public class SegmentLoadingConfig
     this.useRoundRobinSegmentAssignment = useRoundRobinSegmentAssignment;
     this.balancerComputeThreads = balancerComputeThreads;
     this.historicalTierAliases = historicalTierAliases;
+    this.tierToAliasName = tierToAliasName;
   }
 
   public int getMaxSegmentsInLoadQueue()
@@ -128,4 +133,13 @@ public class SegmentLoadingConfig
   {
     return historicalTierAliases;
   }
+
+  /**
+   * Maps each physical tier to the alias tier it belongs to. Used to tag
+   * coordinator metrics with {@link 
org.apache.druid.server.coordinator.stats.Dimension#TIER_ALIAS}.
+   */
+  public Map<String, String> getTierToAliasName()
+  {
+    return tierToAliasName;
+  }
 }
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 9aee83e92f2..fc6936e4df1 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
@@ -66,6 +66,7 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
 
   private final boolean useRoundRobinAssignment;
   private final Map<String, Set<String>> historicalTierAliases;
+  private final Map<String, String> tierToAliasName;
 
   private final Map<String, Set<String>> datasourceToInvalidLoadTiers = new 
HashMap<>();
   private final Map<String, Integer> tierToHistoricalCount = new HashMap<>();
@@ -90,6 +91,7 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
     this.useRoundRobinAssignment = 
loadingConfig.isUseRoundRobinSegmentAssignment();
     this.serverSelector = useRoundRobinAssignment ? new 
RoundRobinServerSelector(cluster) : null;
     this.historicalTierAliases = loadingConfig.getHistoricalTierAliases();
+    this.tierToAliasName = loadingConfig.getTierToAliasName();
 
     cluster.getManagedHistoricals().forEach(
         (tier, historicals) -> tierToHistoricalCount.put(tier, 
historicals.size())
@@ -654,11 +656,25 @@ public class StrategicSegmentAssigner implements 
SegmentActionHandler
 
   private void reportTierCapacityStats(DataSegment segment, int 
requiredReplicas, String tier)
   {
-    final RowKey rowKey = RowKey.of(Dimension.TIER, tier);
+    final RowKey rowKey = tierRowKey(tier);
     stats.updateMax(Stats.Tier.REPLICATION_FACTOR, rowKey, requiredReplicas);
     stats.add(Stats.Tier.REQUIRED_CAPACITY, rowKey, segment.getSize() * 
requiredReplicas);
   }
 
+  /**
+   * Builds a {@link RowKey} for the given physical tier, additionally tagging 
it
+   * with {@link Dimension#TIER_ALIAS} when the tier belongs to an alias. This 
lets
+   * metrics for aliased tiers (e.g. blue/green pairs) be aggregated by alias.
+   */
+  private RowKey tierRowKey(String tier)
+  {
+    final String alias = tierToAliasName.get(tier);
+    if (alias == null) {
+      return RowKey.of(Dimension.TIER, tier);
+    }
+    return RowKey.with(Dimension.TIER, tier).and(Dimension.TIER_ALIAS, alias);
+  }
+
   @Override
   public void broadcastSegment(DataSegment segment)
   {
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/stats/Dimension.java 
b/server/src/main/java/org/apache/druid/server/coordinator/stats/Dimension.java
index 2871aab26d8..dd06e176126 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/stats/Dimension.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/stats/Dimension.java
@@ -25,6 +25,7 @@ package org.apache.druid.server.coordinator.stats;
 public enum Dimension
 {
   TIER("tier"),
+  TIER_ALIAS("tierAlias"),
   TASK_TYPE("taskType"),
   DATASOURCE("dataSource"),
   DUTY("duty"),
diff --git 
a/server/src/test/java/org/apache/druid/server/coordinator/rules/LoadRuleTest.java
 
b/server/src/test/java/org/apache/druid/server/coordinator/rules/LoadRuleTest.java
index 329f24d420e..1dce14d1dda 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordinator/rules/LoadRuleTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordinator/rules/LoadRuleTest.java
@@ -43,6 +43,8 @@ import 
org.apache.druid.server.coordinator.loading.SegmentLoadQueueManager;
 import org.apache.druid.server.coordinator.loading.StrategicSegmentAssigner;
 import org.apache.druid.server.coordinator.loading.TestLoadQueuePeon;
 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 org.apache.druid.timeline.partition.NoneShardSpec;
@@ -557,6 +559,46 @@ public class LoadRuleTest
     Assert.assertEquals(1L, stats.getSegmentStat(Stats.Segments.ASSIGNED, 
Tier.T3, TestDataSource.WIKI));
   }
 
+  /**
+   * Verifies that tier capacity metrics for tiers belonging to an alias are 
tagged
+   * with the {@link Dimension#TIER_ALIAS} dimension, so they can be 
aggregated by
+   * alias, while the physical tier dimension is still present.
+   */
+  @Test
+  public void testHistoricalTierAliasesTagsCapacityStatsWithAlias()
+  {
+    // T1 is the virtual alias key; T2 and T3 are the real tiers it expands to
+    final ServerHolder hot1Server = createServer(Tier.T2);
+    final ServerHolder hot2Server = createServer(Tier.T3);
+    DruidCluster cluster = DruidCluster
+        .builder()
+        .addTier(Tier.T2, hot1Server)
+        .addTier(Tier.T3, hot2Server)
+        .build();
+
+    final DataSegment segment = createDataSegment(TestDataSource.WIKI);
+    LoadRule rule = loadForever(ImmutableMap.of(Tier.T1, 1));
+    CoordinatorRunStats stats = runRuleAndGetStats(
+        rule,
+        segment,
+        makeCoordinatorRuntimeParams(
+            cluster,
+            ImmutableMap.of(Tier.T1, Set.of(Tier.T2, Tier.T3)),
+            segment
+        )
+    );
+
+    // Required capacity is reported against the physical tier AND tagged with 
the alias
+    final RowKey t2WithAlias = RowKey.with(Dimension.TIER, 
Tier.T2).and(Dimension.TIER_ALIAS, Tier.T1);
+    final RowKey t3WithAlias = RowKey.with(Dimension.TIER, 
Tier.T3).and(Dimension.TIER_ALIAS, Tier.T1);
+    Assert.assertEquals(segment.getSize(), 
stats.get(Stats.Tier.REQUIRED_CAPACITY, t2WithAlias));
+    Assert.assertEquals(segment.getSize(), 
stats.get(Stats.Tier.REQUIRED_CAPACITY, t3WithAlias));
+
+    // The same stat without the alias dimension is a different row and must 
be absent
+    Assert.assertEquals(0L, stats.get(Stats.Tier.REQUIRED_CAPACITY, 
RowKey.of(Dimension.TIER, Tier.T2)));
+    Assert.assertEquals(0L, stats.get(Stats.Tier.REQUIRED_CAPACITY, 
RowKey.of(Dimension.TIER, Tier.T3)));
+  }
+
   /**
    * Verifies that an alias key tier with no servers does not fire an 
invalid-tier
    * alert, but an alias value tier with no servers does.
diff --git 
a/server/src/test/java/org/apache/druid/server/coordinator/simulate/HistoricalTierAliasTest.java
 
b/server/src/test/java/org/apache/druid/server/coordinator/simulate/HistoricalTierAliasTest.java
new file mode 100644
index 00000000000..3564719c6b0
--- /dev/null
+++ 
b/server/src/test/java/org/apache/druid/server/coordinator/simulate/HistoricalTierAliasTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.simulate;
+
+import org.apache.druid.client.DruidServer;
+import org.apache.druid.segment.TestDataSource;
+import org.apache.druid.server.coordinator.CoordinatorDynamicConfig;
+import org.apache.druid.server.coordinator.stats.Dimension;
+import org.apache.druid.server.coordinator.stats.Stats;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Verifies that when historical tiers are grouped under an alias (e.g. a 
blue/green
+ * pair), the coordinator tags the per-tier capacity metrics with the
+ * {@link Dimension#TIER_ALIAS} dimension so they can be aggregated by alias.
+ */
+public class HistoricalTierAliasTest extends CoordinatorSimulationBaseTest
+{
+  private static final long SIZE_1TB = 1_000_000;
+  private static final String ALIAS = "hot";
+
+  private DruidServer historicalT1;
+  private DruidServer historicalT2;
+
+  private final String datasource = TestDataSource.WIKI;
+
+  @Override
+  public void setUp()
+  {
+    // T1 and T2 are interchangeable physical tiers grouped under the alias 
"hot"
+    historicalT1 = createHistorical(1, Tier.T1, SIZE_1TB);
+    historicalT2 = createHistorical(1, Tier.T2, SIZE_1TB);
+  }
+
+  @Test
+  public void testCapacityMetricsAreTaggedWithAlias()
+  {
+    final CoordinatorSimulation sim =
+        CoordinatorSimulation.builder()
+                             .withSegments(Segments.WIKI_10X1D)
+                             .withServers(historicalT1, historicalT2)
+                             // Rule targets the virtual alias, which expands 
to T1 and T2
+                             .withRules(datasource, Load.on(ALIAS, 
1).forever())
+                             .withDynamicConfig(
+                                 CoordinatorDynamicConfig.builder()
+                                                         
.withHistoricalTierAliases(
+                                                             Map.of(ALIAS, 
Set.of(Tier.T1, Tier.T2))
+                                                         )
+                                                         
.withSmartSegmentLoading(true)
+                                                         .build()
+                             )
+                             .withImmediateSegmentLoading(true)
+                             .build();
+
+    startSimulation(sim);
+    runCoordinatorCycle();
+
+    final long expectedCapacity = SIZE_1TB << 20;
+
+    // tier/total/capacity is emitted per physical tier AND tagged with the 
alias
+    verifyValue(
+        Stats.Tier.TOTAL_CAPACITY.getMetricName(),
+        Map.of(Dimension.TIER.reportedName(), Tier.T1, 
Dimension.TIER_ALIAS.reportedName(), ALIAS),
+        expectedCapacity
+    );
+    verifyValue(
+        Stats.Tier.TOTAL_CAPACITY.getMetricName(),
+        Map.of(Dimension.TIER.reportedName(), Tier.T2, 
Dimension.TIER_ALIAS.reportedName(), ALIAS),
+        expectedCapacity
+    );
+
+    // tier/historical/count carries the alias too, so it can be summed across 
the pair
+    verifyValue(
+        Stats.Tier.HISTORICAL_COUNT.getMetricName(),
+        Map.of(Dimension.TIER.reportedName(), Tier.T1, 
Dimension.TIER_ALIAS.reportedName(), ALIAS),
+        1L
+    );
+    verifyValue(
+        Stats.Tier.HISTORICAL_COUNT.getMetricName(),
+        Map.of(Dimension.TIER.reportedName(), Tier.T2, 
Dimension.TIER_ALIAS.reportedName(), ALIAS),
+        1L
+    );
+  }
+
+  @Test
+  public void testCapacityMetricsHaveNoAliasWhenNotConfigured()
+  {
+    final CoordinatorSimulation sim =
+        CoordinatorSimulation.builder()
+                             .withSegments(Segments.WIKI_10X1D)
+                             .withServers(historicalT1, historicalT2)
+                             .withRules(datasource, Load.on(Tier.T1, 
1).andOn(Tier.T2, 1).forever())
+                             .withDynamicConfig(
+                                 CoordinatorDynamicConfig.builder()
+                                                         
.withSmartSegmentLoading(true)
+                                                         .build()
+                             )
+                             .withImmediateSegmentLoading(true)
+                             .build();
+
+    startSimulation(sim);
+    runCoordinatorCycle();
+
+    final long expectedCapacity = SIZE_1TB << 20;
+
+    // Without an alias configured, capacity is reported against the physical 
tier only
+    verifyValue(
+        Stats.Tier.TOTAL_CAPACITY.getMetricName(),
+        filterByTier(Tier.T1),
+        expectedCapacity
+    );
+    verifyValue(
+        Stats.Tier.TOTAL_CAPACITY.getMetricName(),
+        filterByTier(Tier.T2),
+        expectedCapacity
+    );
+  }
+}
diff --git 
a/server/src/test/java/org/apache/druid/server/http/CoordinatorDynamicConfigTest.java
 
b/server/src/test/java/org/apache/druid/server/http/CoordinatorDynamicConfigTest.java
index f56344ec12e..53dcd009dc6 100644
--- 
a/server/src/test/java/org/apache/druid/server/http/CoordinatorDynamicConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/http/CoordinatorDynamicConfigTest.java
@@ -698,11 +698,55 @@ public class CoordinatorDynamicConfigTest
     Assert.assertTrue("Throws correct virtual tier alias message", 
exception.getMessage().contains("A virtual tier alias cannot be a physical 
tier."));
   }
 
+  @Test
+  public void testHistoricalTierAliasesRejectsTierInMultipleAliases()
+  {
+    Map<String, Set<String>> aliases = Map.of(
+        "hot", Set.of("tier_1", "tier_2"),
+        "warm", Set.of("tier_2", "tier_3")
+    );
+
+    DruidException exception = Assert.assertThrows(
+        DruidException.class,
+        () -> 
CoordinatorDynamicConfig.builder().withHistoricalTierAliases(aliases).build()
+    );
+    Assert.assertTrue(
+        "Throws correct multi-alias message",
+        exception.getMessage().contains("cannot belong to more than one alias")
+    );
+    Assert.assertTrue(
+        "Names the offending tier",
+        exception.getMessage().contains("tier_2")
+    );
+  }
+
+  @Test
+  public void testGetTierToAliasName()
+  {
+    Map<String, Set<String>> aliases = Map.of(
+        "hot", Set.of("hot_1", "hot_2"),
+        "cold", Set.of("cold_1")
+    );
+    CoordinatorDynamicConfig config = CoordinatorDynamicConfig.builder()
+                                                              
.withHistoricalTierAliases(aliases)
+                                                              .build();
+
+    Map<String, String> expected = Map.of(
+        "hot_1", "hot",
+        "hot_2", "hot",
+        "cold_1", "cold"
+    );
+    Assert.assertEquals(expected, config.getTierToAliasName());
+
+    // No aliases configured -> empty reverse map
+    Assert.assertEquals(Map.of(), 
CoordinatorDynamicConfig.builder().build().getTierToAliasName());
+  }
+
   @Test
   public void testEquals()
   {
     EqualsVerifier.forClass(CoordinatorDynamicConfig.class)
-                  .withIgnoredFields("validDebugDimensions")
+                  .withIgnoredFields("validDebugDimensions", "tierToAliasName")
                   .usingGetClass()
                   .verify();
   }


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

Reply via email to